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
- Verify /usr/bin/ditto exists and runs (ditto -h) on this Mac; it is present on standard macOS installs.
- Check free disk space and that $TMPDIR (os.tmpdir()) is writable; clear space or fix TMPDIR if not.
- 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.
- 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
- Only run the updater on standard macOS images where /usr/bin/ditto exists.
- Keep adequate free disk space and a writable TMPDIR.
- Don't run updates under stripped containers or EDR profiles that block ditto.
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
- failed to extract ops origin/main archive
- screen recorder failed; partial file retained
- The menu-bar app did not exit; update cancelled.
- The running helper did not stop; update cancelled.
- application not found — call list_apps for exact names/pids
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)