Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

196 lines
8.0 KiB
TypeScript

/**
* Local dev-install: packages the app from the current checkout and installs
* it into /Applications — the "run it like a real Mac app" loop before
* official distribution. Signing/notarization are not involved; the locally
* built app never carries a quarantine flag, so Gatekeeper doesn't mind.
*
* bun run install:local # plain Sim.app (origin unchanged)
* bun run install:local --local # Sim Local.app → http://localhost:3000
* bun run install:local --dev # Sim Dev.app → https://www.dev.sim.ai
* bun run install:local --staging # Sim Staging.app → https://www.staging.sim.ai
* bun run install:local --prod # Sim.app → https://www.sim.ai
* bun run install:local --no-open # build → install only
*
* Each environment is a separate app (name, bundle id, install path, userData,
* single-instance lock, update feed), so all four can be installed and run
* side by side. The flag both bakes the default server origin into the build
* and writes the app's persisted settings (same as changing the server URL in
* Settings). A running installed copy of the SAME channel is quit before
* replacing; other channels keep running.
*/
import { execFileSync, spawnSync } from 'node:child_process'
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
interface ChannelIdentity {
/** Display + bundle name; also the userData directory name. */
name: string
appId: string
/** Baked default origin + persisted settings origin. Unset = leave as-is. */
origin?: string
}
/** Must stay in sync with APP_NAME_FOR_CHANNEL in src/main/config.ts. */
const CHANNEL_FLAGS: Record<string, ChannelIdentity> = {
'--local': { name: 'Sim Local', appId: 'ai.sim.desktop.local', origin: 'http://localhost:3000' },
'--dev': { name: 'Sim Dev', appId: 'ai.sim.desktop.dev', origin: 'https://www.dev.sim.ai' },
'--staging': {
name: 'Sim Staging',
appId: 'ai.sim.desktop.staging',
origin: 'https://www.staging.sim.ai',
},
// Origin left unset, exactly as official prod builds do, so this install
// resolves the same DEFAULT_ORIGIN (config.ts) and therefore the same cookie
// partition as a real one. Naming an origin here re-creates the drift that
// pinned prod to the apex while the server serves www.
'--prod': { name: 'Sim', appId: 'ai.sim.desktop' },
}
const DEFAULT_IDENTITY: ChannelIdentity = { name: 'Sim', appId: 'ai.sim.desktop' }
const channelFlags = process.argv.filter((arg) => arg in CHANNEL_FLAGS)
if (channelFlags.length > 1) {
console.error(`✖ Pass at most one of ${Object.keys(CHANNEL_FLAGS).join(', ')}`)
process.exit(1)
}
const identity = channelFlags.length === 1 ? CHANNEL_FLAGS[channelFlags[0]] : DEFAULT_IDENTITY
const APP_NAME = `${identity.name}.app`
const INSTALL_PATH = `/Applications/${APP_NAME}`
const RELEASE_DIRS = ['release/mac-universal', 'release/mac-arm64', 'release/mac']
const LOCAL_BUILD_VERSION = Math.floor(Date.now() / 1000).toString()
const LAUNCH_SERVICES_REGISTER =
'/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister'
/** Matches the app's userData path (app.setName(...) in src/main/index.ts). */
const SETTINGS_PATH = join(homedir(), `Library/Application Support/${identity.name}/settings.json`)
function run(command: string, args: string[], env?: Record<string, string>): void {
const result = spawnSync(command, args, {
stdio: 'inherit',
env: env ? { ...process.env, ...env } : process.env,
})
if (result.status !== 0) {
console.error(`\n✖ ${command} ${args.join(' ')} failed`)
process.exit(result.status ?? 1)
}
}
function localBuildStamp(): string {
try {
const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD']).toString().trim()
const dirty = execFileSync('git', ['status', '--porcelain']).toString().trim() ? '+dirty' : ''
return `${sha}${dirty}`
} catch {
return 'unknown'
}
}
function quitInstalledApp(): void {
// Match only processes launched from this channel's installed bundle —
// never the dev instance running out of node_modules/electron, and never
// another channel's install.
const running = spawnSync('pgrep', ['-f', `${INSTALL_PATH}/Contents/MacOS/`]).status === 0
if (!running) return
console.log('• Quitting the running installed app…')
spawnSync('osascript', ['-e', `tell application "${identity.name}" to quit`])
// Poll briefly; fall back to a hard kill so the install never half-replaces
// a live bundle.
for (let i = 0; i < 20; i++) {
if (spawnSync('pgrep', ['-f', `${INSTALL_PATH}/Contents/MacOS/`]).status !== 0) return
execFileSync('sleep', ['0.25'])
}
spawnSync('pkill', ['-f', `${INSTALL_PATH}/Contents/MacOS/`])
}
/**
* Points the installed app at an environment by writing its persisted
* settings — the same field the in-app Settings window edits. Merges into the
* existing file so window bounds and other settings survive.
*/
function applyOrigin(origin: string): void {
let settings: Record<string, unknown> = {}
try {
settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8')) as Record<string, unknown>
} catch {
// Missing or corrupt settings file — start fresh; the app validates on load.
}
settings.origin = origin
mkdirSync(dirname(SETTINGS_PATH), { recursive: true })
writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\n`)
console.log(`• Server origin set to ${origin}`)
if (origin.startsWith('http://localhost')) {
console.log(' (make sure the sim dev server is running on :3000)')
}
}
function refreshInstalledIcon(): void {
run('touch', [INSTALL_PATH])
if (existsSync(LAUNCH_SERVICES_REGISTER)) {
run(LAUNCH_SERVICES_REGISTER, ['-f', INSTALL_PATH])
}
}
console.log(`• Packaging ${identity.name} from the current checkout…`)
run(
'bun',
['run', 'build'],
identity.origin ? { SIM_DESKTOP_DEFAULT_ORIGIN: identity.origin } : undefined
)
// electron-builder only writes the output dir for the CURRENT target/arch
// (e.g. mac-arm64); other release dirs from older runs (a universal dmg
// build, an Intel machine) would survive and win the pick below. Remove all
// candidates first so the only app found is the one just built.
for (const dir of RELEASE_DIRS) {
rmSync(dir, { recursive: true, force: true })
}
// Same as package:dir, minus trusted timestamps: codesign's --timestamp does
// a network round trip to Apple PER FILE (hundreds inside the Electron
// framework), which turns local signing into a multi-minute stall. Local
// installs don't need timestamped signatures — only notarized distribution
// builds do.
run('bunx', [
'electron-builder',
'--mac',
'dir',
'--publish',
'never',
'-c.mac.timestamp=none',
`-c.productName=${identity.name}`,
`-c.appId=${identity.appId}`,
`-c.buildVersion=${LOCAL_BUILD_VERSION}`,
])
const builtApp = RELEASE_DIRS.map((dir) => join(dir, APP_NAME)).find(existsSync)
if (!builtApp) {
console.error(`✖ No built app found under ${RELEASE_DIRS.join(', ')}`)
process.exit(1)
}
// Without a Developer ID identity electron-builder skips signing entirely,
// and its fuse-flip step invalidates Electron's shipped ad-hoc seal — Apple
// silicon then SIGKILLs the binary at launch (Code Signature Invalid).
// Re-seal the whole bundle ad-hoc; ditto below preserves the signature.
console.log('• Ad-hoc signing the bundle…')
run('codesign', ['--force', '--deep', '--sign', '-', builtApp])
quitInstalledApp()
console.log(`• Installing ${builtApp}${INSTALL_PATH}`)
rmSync(INSTALL_PATH, { recursive: true, force: true })
// ditto preserves the code signature and extended attributes, unlike cp.
run('ditto', [builtApp, INSTALL_PATH])
refreshInstalledIcon()
if (identity.origin) {
applyOrigin(identity.origin)
}
console.log(`✔ Installed ${identity.name} (${localBuildStamp()}) to ${INSTALL_PATH}`)
if (!process.argv.includes('--no-open')) {
run('open', [INSTALL_PATH])
} else {
console.log(` Launch it with: open ${INSTALL_PATH}`)
}