Skip to content

Commit 9a6a404

Browse files
cameroncookeclaude
andcommitted
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>
1 parent c40789b commit 9a6a404

File tree

11 files changed

+1552
-3
lines changed

11 files changed

+1552
-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 runs the appropriate upgrade command. 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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,55 @@ 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
3134
```
3235

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
46+
```
47+
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+
### Install method behavior
58+
59+
The command detects how XcodeBuildMCP was installed and adapts accordingly:
60+
61+
| Method | Auto-upgrade | Command |
62+
|--------|--------------|----------|
63+
| Homebrew | Yes | `brew update && brew upgrade xcodebuildmcp` |
64+
| npm global | Yes | `npm install -g xcodebuildmcp@latest` |
65+
| npx | No | npx resolves `@latest` on each run; update the pinned version in your client config if needed. |
66+
| Unknown | No | Manual instructions for all supported channels are shown. |
67+
68+
### Non-interactive mode
69+
70+
When stdin is not a TTY (CI, pipes, scripts):
71+
72+
- `--check` works normally and exits 0.
73+
- `--yes` runs the upgrade for Homebrew and npm-global installs.
74+
- Without `--check` or `--yes`, the command prints the manual upgrade command and exits 1 (it cannot prompt for confirmation).
75+
76+
### GitHub API failures
77+
78+
If the release check fails (network error, rate limit, timeout), the command prints the detected install method and manual upgrade instructions, then exits 1.
79+
3380
## Tool Options
3481

3582
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)