Hmbown/CodeWhale · error · Error

Check for an available update first.

Error message

Check for an available update first.

What it means

prepareUpdate requires an update object previously produced by checkForUpdate/releaseUpdate (which sets `available:true` plus version, url, sha256 and size). Passing undefined/null or an object from a check that found nothing newer throws immediately, before any download or identity validation. The library never assumes a default update; the caller must run the check first.

Solutions

  1. Call checkForUpdate() and only invoke prepareUpdate with its result when `result.available === true`.
  2. Persist the full result object (including `available`, `version`, `url`, `sha256`, `size`) and pass it verbatim; don't reconstruct a partial object.
  3. Gate the install UI on the availability message so the action can't fire when no update was found.
  4. If the result may be stale, re-run checkForUpdate immediately before prepareUpdate.

Example fix

// before
const update = readUpdateResult();
await prepareUpdate(update); // may be {available:false}
// after
const update = readUpdateResult();
if (update?.available) await prepareUpdate(update);
Defensive patterns

Strategy: type-guard

Validate before calling

if (!update || update.available !== true) throw new Error("Run checkForUpdate first; only available updates can be prepared");

Type guard

function isAvailableUpdate(u){return !!u && typeof u === "object" && u.available === true && typeof u.url === "string" && typeof u.sha256 === "string" && typeof u.version === "string";}

Try / catch

try { await prepareUpdate(update); } catch (e) { if (e.message === "Check for an available update first.") { await checkForUpdate().then(u => u.available ? prepareUpdate(u) : notify(u.message)); } else throw e; }

Prevention

When it happens

Trigger: Calling prepareUpdate() with no argument, with `null`/`undefined`, or with the object returned when no update was available (`{available:false,...}`); storing the check result and calling prepareUpdate after it resolved to available:false.

Common situations: UI code invoking install on startup before checkForUpdate resolves; a user clicking 'Install' when the earlier check said 'no newer version'; passing a hand-rolled object missing the `available` flag; race where the stored result came from an older check.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/1b92cb1600af7337. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/app/updates.mjs:85

    const localLength=bytes.readUInt16LE(offset+26),localExtra=bytes.readUInt16LE(offset+28);
    if(offset+30+localLength+localExtra+compressed>bytes.readUInt32LE(end+16)||bytes.subarray(offset+30,offset+30+localLength).toString("utf8")!==name) throw new Error("Inconsistent update file header.");
    if(bytes.readUInt16LE(offset+8)!==method||bytes.readUInt16LE(offset+6)!==flags||(!(flags&8)&&(bytes.readUInt32LE(offset+18)!==compressed||bytes.readUInt32LE(offset+22)!==size))) throw new Error("Inconsistent update sizes or compression.");
    const start=offset+30+localLength+localExtra;
    // Header sizes are untrusted. Bound actual expansion before ditto writes
    // anything, including a compressed payload whose headers understate size.
    const payload=bytes.subarray(start,start+compressed);
    let expanded;
    try { expanded=method===0?payload.length:inflateRawSync(payload,{maxOutputLength:Math.max(size,1)}).length; }
    catch { throw new Error("Invalid or oversized compressed update entry."); }
    if(expanded!==size) throw new Error("The update entry size did not match its contents.");
    position+=46+length+extra+comment;
  }
  if(position!==end) throw new Error("Invalid update archive length.");
  return count;
}

export async function prepareUpdate(update) {
  if(!update?.available) throw new Error("Check for an available update first.");
  if(!newerVersion(update.version,APP_VERSION)||update.url!==`${repository}/releases/download/v${update.version}/Codewhale-Computer-Use-${update.version}-macos-universal.zip`||!Number.isSafeInteger(update.size)||update.size<=0||update.size>limit) throw new Error("The update identity is invalid.");
  // Only GitHub's fixed release URL and its asset CDN can serve the bytes.
  let url=update.url, response;
  for(let redirects=0;redirects<4;redirects++) {
    response=await fetch(url,{redirect:"manual",signal:AbortSignal.timeout(60_000)});
    if(![301,302,303,307,308].includes(response.status)) break;
    const next=new URL(response.headers.get("location"),url);
    if(next.protocol!=="https:"||!["github.com","release-assets.githubusercontent.com","objects.githubusercontent.com"].includes(next.hostname)) throw new Error("The update download redirected to an unexpected host.");
    url=next.href;
  }
  if(!response?.ok) throw new Error("The update could not be downloaded. Your current app is unchanged.");
  const bytes=await responseBytes(response,update.size);
  if(bytes.length!==update.size||crypto.createHash("sha256").update(bytes).digest("hex")!==update.sha256) throw new Error("The update checksum did not match. Your current app is unchanged.");
  validateReleaseZip(bytes);
  const stage=fs.mkdtempSync(path.join(os.tmpdir(),"codewhale-cu-release-"));
  try {
    const archive=path.join(stage,"release.zip"); fs.writeFileSync(archive,bytes,{mode:0o600});
    const result=spawnSync("ditto",["-x","-k",archive,stage],{encoding:"utf8"});

View on GitHub (pinned to 73e0f67d83)