fix(opencode): name the release when upgrading OpenCode

Since opencode 1.18.x, `POST /global/upgrade` requires a `target` semver in
the body. OpenChamber sent an empty object, so every "Update OpenCode" click
came back 400. The rejection arrives as `{name, data:{message}}`, which has
no `error` field, so the user was left with the bare status text: "Bad
Request".

Resolve the target from the latest release — the same lookup the upgrade
prompt already uses to decide there is anything to offer — and fail with an
explicit code when it cannot be resolved, rather than sending a body opencode
is guaranteed to reject. Read the upstream rejection message so a refused
upgrade explains itself.

The VS Code extension carries its own copy of this flow and had the same two
defects; both are fixed there.

fixes #3121
This commit is contained in:
Iuliia Ivashko
2026-08-28 18:48:27 +03:00
parent 6950e113f4
commit 20cda28cac
6 changed files with 242 additions and 22 deletions
+53 -5
View File
@@ -164,6 +164,41 @@ ${desktopReturn ? `<a class="return" href="openchamber://focus/mcp-auth">Return
return versions.sort((left, right) => compareVersions(right, left))[0];
};
// OpenCode's `/global/upgrade` requires an explicit semver target and rejects
// a bodyless call, so "update to the latest" has to name the version. The
// release lookup is the same one the upgrade-status check already uses to
// decide there is anything to offer.
const resolveOpenCodeUpgradeTarget = async (requestedTarget) => {
if (typeof requestedTarget === 'string' && requestedTarget.trim().length > 0) {
return { resolved: true, target: requestedTarget.trim() };
}
try {
const latest = await fetchLatestOpenCodeVersion();
if (!latest) {
return { resolved: false, reason: 'The latest OpenCode version could not be determined.' };
}
return { resolved: true, target: latest };
} catch (error) {
return {
resolved: false,
reason: error instanceof Error ? error.message : 'The latest OpenCode version could not be determined.',
};
}
};
// OpenCode reports a rejected upgrade as `{ name, data: { message, kind } }`,
// which carries no `error` field. Reading only `error` left the user with the
// bare HTTP status text ("Bad Request") and nothing to act on.
const readOpenCodeUpgradeErrorMessage = (payload, response) => {
const candidates = [payload?.error, payload?.data?.message, payload?.message];
for (const candidate of candidates) {
if (typeof candidate === 'string' && candidate.trim().length > 0) {
return candidate.trim();
}
}
return response.statusText || 'Failed to upgrade OpenCode';
};
const pruneExpiredPendingMcpAuthContexts = () => {
const now = Date.now();
for (const [state, entry] of pendingMcpAuthContextByState.entries()) {
@@ -218,10 +253,23 @@ ${desktopReturn ? `<a class="return" href="openchamber://focus/mcp-auth">Return
});
}
const target = typeof req.body?.target === 'string' && req.body.target.trim().length > 0
? req.body.target.trim()
: undefined;
const requestedTarget = req.body?.target;
// The target lookup reaches the network, so it runs inside the operation:
// the in-flight lock is taken synchronously above, and a second click
// cannot slip past while the release version is being resolved.
const upgradeOperation = (async () => {
const targetResolution = await resolveOpenCodeUpgradeTarget(requestedTarget);
if (!targetResolution.resolved) {
return {
status: 502,
body: {
success: false,
code: 'OPENCODE_UPGRADE_TARGET_UNRESOLVED',
error: `Could not determine which OpenCode version to install: ${targetResolution.reason}`,
},
};
}
const response = await fetch(buildOpenCodeUrl('/global/upgrade', ''), {
method: 'POST',
headers: {
@@ -229,7 +277,7 @@ ${desktopReturn ? `<a class="return" href="openchamber://focus/mcp-auth">Return
Accept: 'application/json',
...getOpenCodeAuthHeaders(),
},
body: JSON.stringify(target ? { target } : {}),
body: JSON.stringify({ target: targetResolution.target }),
});
const payload = await response.json().catch(() => null);
if (!response.ok) {
@@ -237,7 +285,7 @@ ${desktopReturn ? `<a class="return" href="openchamber://focus/mcp-auth">Return
status: response.status,
body: {
success: false,
error: payload?.error || response.statusText || 'Failed to upgrade OpenCode',
error: readOpenCodeUpgradeErrorMessage(payload, response),
},
};
}