Skip to content

Commit f2ff839

Browse files
author
test2
committed
feat: add latest parameter for xcodes and update CLUADE.md
1 parent 1758d57 commit f2ff839

5 files changed

Lines changed: 97 additions & 9 deletions

File tree

CLAUDE.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,46 @@ parameterSettings: {
501501
}
502502
```
503503

504+
### "latest" Keyword for Version-List Parameters
505+
506+
When a resource manages a list of installed versions (e.g. `nvm`'s Node versions, `pyenv`'s Python versions, `xcodes`' Xcode versions), always support a symbolic `'latest'` entry in that array alongside explicit version strings. This lets users write `versions: ['latest']` instead of having to know/hardcode the current newest release.
507+
508+
**Requirements for `'latest'`:**
509+
- It must resolve to a real, concrete version at `addItem`/install time (e.g. by passing whatever "install latest" flag the underlying CLI supports — `xcodes install --latest`, `nvm install --lts`/`node`, `pyenv install` + `pyenv latest -k <major>`, etc. — or by resolving the latest version yourself before installing if the CLI has no such flag).
510+
- It must **not** show up as a perpetual diff in the plan. Once resolved, `refresh()` should normalize the real installed version back to the literal string `'latest'` in the array it returns whenever that installed version is the one which satisfies the `'latest'` entry in desired — so the framework's equality check treats them as converged instead of proposing an add/remove on every plan.
511+
- `removeItem` (and any other lifecycle method that receives an individual array element) must resolve `'latest'` back to the real installed version before acting — never pass the literal string `'latest'` to an uninstall/select command.
512+
513+
**Reference implementation:** `src/resources/xcodes/xcode-versions-parameter.ts` (`LATEST_VERSION_KEYWORD`, `normalizeLatestKeyword`, `resolveInstalledVersion`). The pattern:
514+
515+
```typescript
516+
export const LATEST_VERSION_KEYWORD = 'latest';
517+
518+
export class MyVersionsParameter extends ArrayStatefulParameter<MyConfig, string> {
519+
getSettings(): ArrayParameterSetting {
520+
return { type: 'array', isElementEqual: (desired, current) => desired === current };
521+
}
522+
523+
override async refresh(desired: string[] | null): Promise<string[] | null> {
524+
const installed = await getInstalledVersions();
525+
return normalizeLatestKeyword(installed, desired ?? []); // maps the newest unclaimed installed version back to 'latest'
526+
}
527+
528+
override async addItem(version: string): Promise<void> {
529+
const installArg = version === LATEST_VERSION_KEYWORD ? '--latest' : version;
530+
await install(installArg);
531+
}
532+
533+
override async removeItem(version: string): Promise<void> {
534+
const resolved = version === LATEST_VERSION_KEYWORD ? await resolveNewestInstalled() : version;
535+
if (resolved) await uninstall(resolved);
536+
}
537+
}
538+
```
539+
540+
Also add `'latest'` as a hardcoded first entry in that parameter's completions file (`completions/<resource>.$.<param>.ts`) so it surfaces as a suggestion in the editor alongside real fetched version numbers.
541+
542+
Do **not** extend this convention to a resource's singular "selected/active version" parameter (e.g. `xcodes`' `selected`) unless the underlying CLI's select/activate command itself supports a latest-equivalent flag — most select commands only operate on already-installed exact versions.
543+
504544
### defaultConfig and exampleConfigs
505545

506546
Every resource should have a `defaultConfig` and `exampleConfigs`. These are surfaced in the Codify Editor to help users get started quickly.

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.2",
3+
"version": "1.15.3-beta.3",
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/completions/xcodes.$.xcodeVersions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ function toXcodesVersionString(release: XcodeRelease): string {
1919
export default async function loadXcodeVersions(): Promise<string[]> {
2020
const response = await fetch(XCODE_RELEASES_URL);
2121
const releases = await response.json() as XcodeRelease[];
22-
return releases.map(toXcodesVersionString);
22+
// "latest" is a hardcoded sentinel supported by the xcodes resource
23+
// (maps to `xcodes install --latest`), not a real xcodereleases.com entry.
24+
return ['latest', ...releases.map(toXcodesVersionString)];
2325
}

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

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

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

5+
export const LATEST_VERSION_KEYWORD = 'latest';
6+
57
export class XcodeVersionsParameter extends ArrayStatefulParameter<XcodesConfig, string> {
6-
override async refresh(_desired: string[] | null): Promise<string[] | null> {
8+
getSettings(): ArrayParameterSetting {
9+
return {
10+
type: 'array',
11+
// "latest" never matches a real version string returned by refresh() on its own;
12+
// refresh() below re-normalizes whichever installed version fulfilled "latest"
13+
// back into the literal string "latest" so the framework treats them as equal.
14+
isElementEqual: (desired, current) => desired === current,
15+
};
16+
}
17+
18+
override async refresh(desired: string[] | null): Promise<string[] | null> {
719
const $ = getPty();
820
const { data } = await $.spawnSafe('xcodes installed');
9-
return parseInstalledVersions(data);
21+
const installed = parseInstalledVersions(data);
22+
return normalizeLatestKeyword(installed, desired ?? []);
1023
}
1124

1225
override async addItem(version: string, plan: Plan<XcodesConfig>): Promise<void> {
@@ -17,17 +30,28 @@ export class XcodeVersionsParameter extends ArrayStatefulParameter<XcodesConfig,
1730
if (appleId) env['XCODES_USERNAME'] = appleId;
1831
if (appleIdPassword) env['XCODES_PASSWORD'] = appleIdPassword;
1932

20-
await $.spawn(`xcodes install "${version}"`, {
33+
const installArg = version === LATEST_VERSION_KEYWORD ? '--latest' : `"${version}"`;
34+
await $.spawn(`xcodes install ${installArg}`, {
2135
interactive: true,
2236
stdin: true,
2337
...(Object.keys(env).length > 0 ? { env } : {}),
2438
});
2539

2640
if (acceptLicense !== false) {
27-
await this.acceptLicenseIfNeeded(version);
41+
const installedVersion = await this.resolveInstalledVersion(version);
42+
if (installedVersion) await this.acceptLicenseIfNeeded(installedVersion);
2843
}
2944
}
3045

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+
3155
private async acceptLicenseIfNeeded(version: string): Promise<void> {
3256
const $ = getPty();
3357

@@ -45,7 +69,9 @@ export class XcodeVersionsParameter extends ArrayStatefulParameter<XcodesConfig,
4569

4670
override async removeItem(version: string): Promise<void> {
4771
const $ = getPty();
48-
await $.spawn(`xcodes uninstall "${version}"`, { interactive: true });
72+
const installedVersion = await this.resolveInstalledVersion(version);
73+
if (!installedVersion) return;
74+
await $.spawn(`xcodes uninstall "${installedVersion}"`, { interactive: true });
4975
}
5076
}
5177

@@ -60,3 +86,20 @@ function parseInstalledVersions(output: string): string[] {
6086
})
6187
.filter((v): v is string => v !== null);
6288
}
89+
90+
/**
91+
* Replaces whichever installed version fulfills the "latest" sentinel with the
92+
* literal string "latest" so the framework's equality check (desired === current)
93+
* treats them as converged, instead of endlessly re-adding/removing.
94+
*/
95+
function normalizeLatestKeyword(installed: string[], desired: string[]): string[] {
96+
if (!desired.includes(LATEST_VERSION_KEYWORD)) return installed;
97+
98+
const unclaimed = installed.filter((v) => !desired.includes(v));
99+
if (unclaimed.length === 0) return installed;
100+
101+
// xcodes installed lists oldest-to-newest; the newest unclaimed version is
102+
// the one that satisfies "latest".
103+
const latestMatch = unclaimed.at(-1)!;
104+
return installed.map((v) => (v === latestMatch ? LATEST_VERSION_KEYWORD : v));
105+
}

src/resources/xcodes/xcodes-resource.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ const schema = z
1717
.object({
1818
xcodeVersions: z
1919
.array(z.string())
20-
.describe('List of Xcode versions to install via xcodes (e.g. ["15.2", "14.3.1"]).')
20+
.describe(
21+
'List of Xcode versions to install via xcodes (e.g. ["15.2", "14.3.1"]). ' +
22+
'Use "latest" to install the newest available Xcode release (runs `xcodes install --latest`).'
23+
)
2124
.optional(),
2225
selected: z
2326
.string()

0 commit comments

Comments
 (0)