|
| 1 | +/** |
| 2 | + * Local dev-install: packages the app from the current checkout and replaces |
| 3 | + * /Applications/Sim.app with it — the "run it like a real Mac app" loop |
| 4 | + * before official distribution. Signing/notarization are not involved; the |
| 5 | + * locally built app never carries a quarantine flag, so Gatekeeper doesn't |
| 6 | + * mind. |
| 7 | + * |
| 8 | + * bun run install:local # build → install → open (origin unchanged) |
| 9 | + * bun run install:local --local # …pointed at http://localhost:3000 |
| 10 | + * bun run install:local --dev # …pointed at https://www.dev.sim.ai |
| 11 | + * bun run install:local --staging # …pointed at https://www.staging.sim.ai |
| 12 | + * bun run install:local --prod # …pointed at https://www.sim.ai |
| 13 | + * bun run install:local --no-open # build → install only |
| 14 | + * |
| 15 | + * The origin flag writes the app's persisted settings (same as changing the |
| 16 | + * server URL in Settings), so it survives relaunches; each origin keeps its |
| 17 | + * own isolated session partition. A running installed copy is quit before |
| 18 | + * replacing. Note the installed app and `bun run dev` share the same profile |
| 19 | + * and single-instance lock, so only one can run at a time. |
| 20 | + */ |
| 21 | +import { execFileSync, spawnSync } from 'node:child_process' |
| 22 | +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' |
| 23 | +import { homedir } from 'node:os' |
| 24 | +import { dirname, join } from 'node:path' |
| 25 | + |
| 26 | +const APP_NAME = 'Sim.app' |
| 27 | +const INSTALL_PATH = `/Applications/${APP_NAME}` |
| 28 | +const RELEASE_DIRS = ['release/mac-universal', 'release/mac-arm64', 'release/mac'] |
| 29 | +/** Matches the app's userData path (app.setName('Sim') in src/main/index.ts). */ |
| 30 | +const SETTINGS_PATH = join(homedir(), 'Library/Application Support/Sim/settings.json') |
| 31 | + |
| 32 | +const ORIGIN_FLAGS: Record<string, string> = { |
| 33 | + '--local': 'http://localhost:3000', |
| 34 | + '--dev': 'https://www.dev.sim.ai', |
| 35 | + '--staging': 'https://www.staging.sim.ai', |
| 36 | + '--prod': 'https://www.sim.ai', |
| 37 | +} |
| 38 | + |
| 39 | +function run(command: string, args: string[]): void { |
| 40 | + const result = spawnSync(command, args, { stdio: 'inherit' }) |
| 41 | + if (result.status !== 0) { |
| 42 | + console.error(`\n✖ ${command} ${args.join(' ')} failed`) |
| 43 | + process.exit(result.status ?? 1) |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +function localBuildStamp(): string { |
| 48 | + try { |
| 49 | + const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD']).toString().trim() |
| 50 | + const dirty = execFileSync('git', ['status', '--porcelain']).toString().trim() ? '+dirty' : '' |
| 51 | + return `${sha}${dirty}` |
| 52 | + } catch { |
| 53 | + return 'unknown' |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +function quitInstalledApp(): void { |
| 58 | + // Match only processes launched from the installed bundle — never the dev |
| 59 | + // instance running out of node_modules/electron. |
| 60 | + const running = spawnSync('pgrep', ['-f', `${INSTALL_PATH}/Contents/MacOS/`]).status === 0 |
| 61 | + if (!running) return |
| 62 | + console.log('• Quitting the running installed app…') |
| 63 | + spawnSync('osascript', ['-e', 'tell application "Sim" to quit']) |
| 64 | + // Poll briefly; fall back to a hard kill so the install never half-replaces |
| 65 | + // a live bundle. |
| 66 | + for (let i = 0; i < 20; i++) { |
| 67 | + if (spawnSync('pgrep', ['-f', `${INSTALL_PATH}/Contents/MacOS/`]).status !== 0) return |
| 68 | + execFileSync('sleep', ['0.25']) |
| 69 | + } |
| 70 | + spawnSync('pkill', ['-f', `${INSTALL_PATH}/Contents/MacOS/`]) |
| 71 | +} |
| 72 | + |
| 73 | +/** |
| 74 | + * Points the installed app at an environment by writing its persisted |
| 75 | + * settings — the same field the in-app Settings window edits. Merges into the |
| 76 | + * existing file so window bounds, shortcuts, etc. survive. |
| 77 | + */ |
| 78 | +function applyOrigin(origin: string): void { |
| 79 | + let settings: Record<string, unknown> = {} |
| 80 | + try { |
| 81 | + settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8')) as Record<string, unknown> |
| 82 | + } catch { |
| 83 | + // Missing or corrupt settings file — start fresh; the app validates on load. |
| 84 | + } |
| 85 | + settings.origin = origin |
| 86 | + mkdirSync(dirname(SETTINGS_PATH), { recursive: true }) |
| 87 | + writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\n`) |
| 88 | + console.log(`• Server origin set to ${origin}`) |
| 89 | + if (origin.startsWith('http://localhost')) { |
| 90 | + console.log(' (make sure the sim dev server is running on :3000)') |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +const originFlags = process.argv.filter((arg) => arg in ORIGIN_FLAGS) |
| 95 | +if (originFlags.length > 1) { |
| 96 | + console.error(`✖ Pass at most one of ${Object.keys(ORIGIN_FLAGS).join(', ')}`) |
| 97 | + process.exit(1) |
| 98 | +} |
| 99 | + |
| 100 | +console.log('• Packaging the app from the current checkout…') |
| 101 | +run('bun', ['run', 'package:dir']) |
| 102 | + |
| 103 | +const builtApp = RELEASE_DIRS.map((dir) => join(dir, APP_NAME)).find(existsSync) |
| 104 | +if (!builtApp) { |
| 105 | + console.error(`✖ No built app found under ${RELEASE_DIRS.join(', ')}`) |
| 106 | + process.exit(1) |
| 107 | +} |
| 108 | + |
| 109 | +quitInstalledApp() |
| 110 | + |
| 111 | +console.log(`• Installing ${builtApp} → ${INSTALL_PATH}`) |
| 112 | +rmSync(INSTALL_PATH, { recursive: true, force: true }) |
| 113 | +// ditto preserves the code signature and extended attributes, unlike cp. |
| 114 | +run('ditto', [builtApp, INSTALL_PATH]) |
| 115 | + |
| 116 | +if (originFlags.length === 1) { |
| 117 | + applyOrigin(ORIGIN_FLAGS[originFlags[0]]) |
| 118 | +} |
| 119 | + |
| 120 | +console.log(`✔ Installed Sim (${localBuildStamp()}) to ${INSTALL_PATH}`) |
| 121 | + |
| 122 | +if (!process.argv.includes('--no-open')) { |
| 123 | + run('open', [INSTALL_PATH]) |
| 124 | +} else { |
| 125 | + console.log(` Launch it with: open ${INSTALL_PATH}`) |
| 126 | +} |
0 commit comments