Skip to content

Commit 03e0a23

Browse files
cameroncookeclaude
andauthored
feat(cli): Add upgrade command for CLI self-update (#348)
* feat(cli): Add upgrade command for CLI self-update Add a new top-level `xcodebuildmcp upgrade` command that compares the installed version against the latest GitHub release, detects the install method (Homebrew, npm-global, npx, or unknown), shows a truncated release-notes excerpt, and either runs the correct upgrade command with inherited stdio or prints the manual command for unsupported channels. Supports `--check` to report versions without prompting and `--yes`/`-y` to skip the interactive confirmation for scripted use. Non-TTY callers that could auto-upgrade but omit `--yes` exit with status 1 so scripts do not hang. Repo owner/name and package name are now emitted from the version generator so the command reads them from package.json rather than hardcoding. The command short-circuits in src/cli.ts to avoid the full runtime bootstrap, mirroring `init` and `setup`. Co-Authored-By: Claude <noreply@anthropic.com> * feat(cli): Make upgrade command channel-aware Each install channel now queries its own source of truth for the latest version instead of always using GitHub Releases. This prevents misleading results when release channels drift — e.g. GitHub may publish a version before the Homebrew tap bumps, or npm dist-tags may lag a pre-release. - Homebrew: `brew info --json=v2` (handles exit-0-with-empty-formulae) - npm-global / npx: `npm view <pkg>@latest version --json` - Unknown: falls back to GitHub Releases latest endpoint Release notes are fetched separately by tag from GitHub and are non-fatal if unavailable (404, timeout, network error). New DI surface: fetchLatestVersionForChannel, fetchReleaseNotesForTag, runChannelLookupCommand (for test isolation). The dependency factory rebuilds derived defaults from merged overrides so tests can mock at any level. Co-Authored-By: Claude <noreply@anthropic.com> * fix(cli): Allow hyphens in semver identifiers and check brew exit code Fix parseVersion regex to accept hyphens in prerelease and build-metadata identifiers as required by SemVer 2.0.0, so versions like 1.0.0-alpha-1 or 1.0.0-rc-1+build-hash no longer fail to parse. Check the exit code of `brew info` before attempting to parse its stdout. Homebrew exits 0 for missing formulas (a quirk the existing empty-formulae path handles), but other failure modes like a broken brew install or permission errors exit non-zero with empty stdout — those now surface a specific exit-code error instead of a misleading "invalid JSON output". Reported in PR review by Cursor Bugbot and Sentry. Co-Authored-By: Claude <noreply@anthropic.com> * fix(cli): Compare prerelease identifiers in ASCII order per SemVer 2.0.0 SemVer 2.0.0 §11 requires lexical comparison of prerelease identifiers in ASCII sort order. The previous implementation used String.prototype.localeCompare, which applies locale-dependent collation and can differ from ASCII ordering. For example, "Alpha".localeCompare("alpha") returns 1 in common locales while ASCII comparison returns -1. Replace localeCompare with a UTF-16 code-unit comparison, which matches ASCII ordering for the [0-9A-Za-z-] character set SemVer permits. Reported in PR review by Cursor Bugbot. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c40789b commit 03e0a23

File tree

11 files changed

+2013
-3
lines changed

11 files changed

+2013
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### Added
6+
7+
- Added `xcodebuildmcp upgrade` command to check for updates and upgrade in place. Supports `--check` (report-only) and `--yes`/`-y` (skip confirmation). Detects install method (Homebrew, npm-global, npx) and queries the appropriate channel source (`brew info`, `npm view`, or GitHub Releases) for the latest version. Non-interactive environments exit 1 when an auto-upgrade is possible but `--yes` was not supplied.
8+
39
## [2.3.2]
410

511
### Fixed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,13 @@ xcodebuildmcp tools
352352
xcodebuildmcp simulator build --scheme MyApp --project-path ./MyApp.xcodeproj
353353
```
354354

355+
Check for updates and upgrade in place:
356+
357+
```bash
358+
xcodebuildmcp upgrade --check
359+
xcodebuildmcp upgrade --yes
360+
```
361+
355362
The CLI uses a per-workspace daemon for stateful operations (log capture, debugging, etc.) that auto-starts when needed. See [docs/CLI.md](docs/CLI.md) for full documentation.
356363

357364
## Documentation

docs/CLI.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,59 @@ xcodebuildmcp <workflow> <tool> --help
2828

2929
# Run interactive setup for .xcodebuildmcp/config.yaml
3030
xcodebuildmcp setup
31+
32+
# Check for updates
33+
xcodebuildmcp upgrade --check
34+
```
35+
36+
## Upgrade
37+
38+
`xcodebuildmcp upgrade` checks for a newer release and optionally runs the upgrade.
39+
40+
```bash
41+
# Check for updates without upgrading
42+
xcodebuildmcp upgrade --check
43+
44+
# Upgrade automatically (skip confirmation prompt)
45+
xcodebuildmcp upgrade --yes
3146
```
3247

48+
### Flags
49+
50+
| Flag | Description |
51+
|------|-------------|
52+
| `--check` | Report the latest version and exit. Never prompts or runs an upgrade. |
53+
| `--yes` / `-y` | Skip the confirmation prompt and run the upgrade command automatically. |
54+
55+
When both `--check` and `--yes` are supplied, `--check` wins.
56+
57+
### Channel-aware version lookup
58+
59+
The version check queries the source of truth for your install channel — `brew info` for Homebrew, `npm view` for npm/npx, or GitHub Releases for unknown installs. This avoids misleading results when release channels drift (e.g. GitHub may publish a version before the Homebrew tap bumps). If the channel-specific lookup fails, the command does not fall back to another source; it reports the error and exits 1.
60+
61+
### Install method behavior
62+
63+
The command detects how XcodeBuildMCP was installed and adapts accordingly:
64+
65+
| Method | Auto-upgrade | Command |
66+
|--------|--------------|----------|
67+
| Homebrew | Yes | `brew update && brew upgrade xcodebuildmcp` |
68+
| npm global | Yes | `npm install -g xcodebuildmcp@latest` |
69+
| npx | No | npx resolves `@latest` on each run; update the pinned version in your client config if needed. |
70+
| Unknown | No | Manual instructions for all supported channels are shown. |
71+
72+
### Non-interactive mode
73+
74+
When stdin is not a TTY (CI, pipes, scripts):
75+
76+
- `--check` works normally and exits 0.
77+
- `--yes` runs the upgrade for Homebrew and npm-global installs.
78+
- Without `--check` or `--yes`, the command prints the manual upgrade command and exits 1 (it cannot prompt for confirmation).
79+
80+
### Lookup failures
81+
82+
If the channel-specific version check fails (network error, rate limit, timeout, missing formula), the command prints the detected install method and manual upgrade instructions, then exits 1.
83+
3384
## Tool Options
3485

3586
Each tool supports `--help` for detailed options:

docs/GETTING_STARTED.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ Using `@latest` ensures clients resolve the newest version on each run.
6262

6363
See [CLI.md](CLI.md) for full CLI documentation.
6464

65+
### Checking for updates
66+
67+
After installing, check for newer releases at any time:
68+
69+
```bash
70+
xcodebuildmcp upgrade --check
71+
```
72+
73+
Homebrew and npm-global installs can auto-upgrade with `xcodebuildmcp upgrade --yes`. npx users don't need to upgrade explicitly — `@latest` resolves the newest version on each run. If you pinned a specific version in your MCP client config, update the version there instead.
74+
6575
## Project config (optional)
6676
For deterministic session defaults and runtime configuration, add a config file at:
6777

scripts/check-docs-cli-commands.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ function extractCommandCandidates(content) {
125125
}
126126

127127
function findInvalidCommands(files, validPairs, validWorkflows) {
128-
const validTopLevel = new Set(['mcp', 'tools', 'daemon', 'init', 'setup']);
128+
const validTopLevel = new Set(['mcp', 'tools', 'daemon', 'init', 'setup', 'upgrade']);
129129
const validDaemonActions = new Set(['status', 'start', 'stop', 'restart', 'list']);
130130
const findings = [];
131131

scripts/generate-version.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,21 @@ import { readFile, writeFile } from 'node:fs/promises';
22
import path from 'node:path';
33

44
interface PackageJson {
5+
name: string;
56
version: string;
67
iOSTemplateVersion: string;
78
macOSTemplateVersion: string;
9+
repository?: {
10+
url?: string;
11+
};
12+
}
13+
14+
function parseGitHubOwnerAndName(url: string): { owner: string; name: string } {
15+
const match = url.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
16+
if (!match) {
17+
throw new Error(`Cannot parse GitHub owner/name from repository URL: ${url}`);
18+
}
19+
return { owner: match[1], name: match[2] };
820
}
921

1022
async function main(): Promise<void> {
@@ -15,10 +27,20 @@ async function main(): Promise<void> {
1527
const raw = await readFile(packagePath, 'utf8');
1628
const pkg = JSON.parse(raw) as PackageJson;
1729

30+
const repoUrl = pkg.repository?.url;
31+
if (!repoUrl) {
32+
throw new Error('package.json must have a repository.url field');
33+
}
34+
35+
const repo = parseGitHubOwnerAndName(repoUrl);
36+
1837
const content =
1938
`export const version = '${pkg.version}';\n` +
2039
`export const iOSTemplateVersion = '${pkg.iOSTemplateVersion}';\n` +
21-
`export const macOSTemplateVersion = '${pkg.macOSTemplateVersion}';\n`;
40+
`export const macOSTemplateVersion = '${pkg.macOSTemplateVersion}';\n` +
41+
`export const packageName = '${pkg.name}';\n` +
42+
`export const repositoryOwner = '${repo.owner}';\n` +
43+
`export const repositoryName = '${repo.name}';\n`;
2244

2345
await writeFile(versionPath, content, 'utf8');
2446
}

src/cli.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ async function runSetupCommand(): Promise<void> {
8080
await app.parseAsync();
8181
}
8282

83+
async function runUpgradeCommand(): Promise<void> {
84+
const { registerUpgradeCommand } = await import('./cli/commands/upgrade.ts');
85+
const app = await buildLightweightYargsApp();
86+
registerUpgradeCommand(app);
87+
await app.parseAsync();
88+
}
89+
8390
async function main(): Promise<void> {
8491
const cliBootstrapStartedAt = Date.now();
8592
const earlyCommand = findTopLevelCommand(process.argv.slice(2));
@@ -95,6 +102,10 @@ async function main(): Promise<void> {
95102
await runSetupCommand();
96103
return;
97104
}
105+
if (earlyCommand === 'upgrade') {
106+
await runUpgradeCommand();
107+
return;
108+
}
98109

99110
await hydrateSentryDisabledEnvFromProjectConfig();
100111
initSentry({ mode: 'cli' });

0 commit comments

Comments
 (0)