* Mount a DMG, copy the .app to /Applications, then detach * @param {string} dmgPath - Path to the DMG file * @returns {Promise } - Path to the installed app
(dmgPath)
| 299 | * @returns {Promise<string>} - Path to the installed app |
| 300 | */ |
| 301 | function installFromDmg (dmgPath) { |
| 302 | return new Promise((resolve, reject) => { |
| 303 | // Step 1: Mount the DMG |
| 304 | console.log(' Mounting DMG...') |
| 305 | execFile('hdiutil', ['attach', dmgPath, '-nobrowse', '-readonly'], (err, stdout) => { |
| 306 | if (err) { |
| 307 | reject(new Error(`Failed to mount DMG: ${err.message}`)) |
| 308 | return |
| 309 | } |
| 310 | |
| 311 | // Parse mount point |
| 312 | const mountMatch = stdout.match(/(\/Volumes\/[^\n]+)/) |
| 313 | if (!mountMatch) { |
| 314 | reject(new Error('Could not find mount point')) |
| 315 | return |
| 316 | } |
| 317 | |
| 318 | const mountPoint = mountMatch[1].trim() |
| 319 | console.log(` Mounted at: ${mountPoint}`) |
| 320 | |
| 321 | // Step 2: Find the .app bundle |
| 322 | try { |
| 323 | const entries = fs.readdirSync(mountPoint) |
| 324 | const appFile = entries.find(e => e.endsWith('.app')) |
| 325 | |
| 326 | if (!appFile) { |
| 327 | // Try to detach before rejecting |
| 328 | execFileSyncIgnore('hdiutil', ['detach', mountPoint]) |
| 329 | reject(new Error('No .app bundle found in DMG')) |
| 330 | return |
| 331 | } |
| 332 | |
| 333 | const appSource = join(mountPoint, appFile) |
| 334 | const appDest = `/Applications/${appFile}` |
| 335 | |
| 336 | // Check if app already exists |
| 337 | if (fs.existsSync(appDest)) { |
| 338 | console.log(` Existing app found at ${appDest}, replacing...`) |
| 339 | // Remove existing app |
| 340 | rm('-rf', appDest) |
| 341 | } |
| 342 | |
| 343 | // Step 3: Copy the app to /Applications |
| 344 | console.log(` Installing ${appFile} to /Applications...`) |
| 345 | execFile('cp', ['-R', appSource, appDest], (cpErr) => { |
| 346 | // Step 4: Detach the DMG (always, regardless of copy result) |
| 347 | console.log(' Detaching DMG...') |
| 348 | execFile('hdiutil', ['detach', mountPoint], (detachErr) => { |
| 349 | if (detachErr) { |
| 350 | console.log(' Warning: Failed to detach DMG:', detachErr.message) |
| 351 | } else { |
| 352 | console.log(' DMG detached') |
| 353 | } |
| 354 | |
| 355 | if (cpErr) { |
| 356 | reject(new Error(`Failed to copy app: ${cpErr.message}`)) |
| 357 | return |
| 358 | } |
no test coverage detected