Hmbown/CodeWhale · error · Error

The update could not be unpacked.

Error message

The update could not be unpacked.

What it means

prepareUpdate() extracts the downloaded release zip using macOS ditto (spawnSync("ditto", ["-x","-k",archive,stage])). A non-zero exit status from ditto means the archive could not be expanded into the staging directory; the stage is cleaned up and the error rethrown. This runs after checksum and zip-structure validation, so it usually indicates an extraction-level problem.

Solutions

  1. Verify /usr/bin/ditto exists and runs (ditto -h) on this Mac; it is present on standard macOS installs.
  2. Check free disk space and that $TMPDIR (os.tmpdir()) is writable; clear space or fix TMPDIR if not.
  3. Re-download the update to rule out an archive that passed sha256 but still trips extraction; if it persists, report the release asset as defective.
  4. Check for security/EDR software blocking ditto execution and add an exception.
Defensive patterns

Strategy: validation

Validate before calling

import {execFileSync} from 'child_process';
try { execFileSync('/usr/bin/ditto', ['--help'], {stdio:'ignore'}); } catch { throw new Error('ditto unavailable; updates unsupported on this host'); }
import statfs from 'node:fs'; // also verify tmpdir writability and free space before starting

Try / catch

try { await prepareUpdate(update); } catch (e) { if (/could not be unpacked/.test(e.message)) { /* check ditto availability, TMPDIR, disk space; report defect if archive itself is bad */ } else throw e; }

Prevention

When it happens

Trigger: ditto is missing or not executable on the host (non-standard macOS image); the staging path in os.tmpdir() is unwritable or full; the zip, though structurally valid, contains entries ditto refuses to extract (permission/resource-fork anomalies); disk space exhausted during extraction.

Common situations: Running under a stripped-down macOS environment or container where /usr/bin/ditto is unavailable; TMPDIR pointed at a read-only or tiny volume; a corrupted-adjacent zip that passed structural checks but trips ditto; security software blocking ditto execution.

Related errors


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

Appendix: source

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

  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"});
    if(result.status!==0) throw new Error("The update could not be unpacked.");
    const bundle=path.join(stage,`${APP_NAME}.app`); verifyReleaseBundle(bundle);
    const version=spawnSync("/usr/libexec/PlistBuddy",["-c","Print :CFBundleShortVersionString",path.join(bundle,"Contents","Info.plist")],{encoding:"utf8"});
    if(version.status!==0||version.stdout.trim()!==update.version) throw new Error("The downloaded app has a different version.");
    return {stage,bundle};
  } catch(error) { fs.rmSync(stage,{recursive:true,force:true}); throw error; }
}

export async function restartWithUpdate(prepared,destination) {
  const logDir=path.join(os.homedir(),"Library","Logs",APP_NAME); fs.mkdirSync(logDir,{recursive:true});
  const log=fs.openSync(path.join(logDir,"update.log"),"a",0o600);
  const child=spawn(process.execPath,[fileURLToPath(import.meta.url),"--apply",prepared.bundle,destination,String(process.pid),String(process.ppid)],{detached:true,stdio:["ignore",log,log]});
  try { await new Promise((resolve,reject)=>{child.once("spawn",resolve);child.once("error",reject);}); child.unref(); }
  finally { fs.closeSync(log); }
}

if(process.argv[1]===fileURLToPath(import.meta.url)&&process.argv[2]==="--apply") {
  const [source,destination,owner,launcher]=process.argv.slice(3);
  let result;

View on GitHub (pinned to 73e0f67d83)