Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
dist/
release/
build/generated-icon.icns
build/generated-icon.icon
playwright-report/
test-results/
2 changes: 1 addition & 1 deletion apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Local unsigned build: `bun run package:dir` (app in `release/mac-universal/`). S

Pre-release share (no Developer ID yet): `SIM_DESKTOP_DEFAULT_ORIGIN=https://www.dev.sim.ai bun run package:share` builds a DMG whose fresh installs default to that origin (baked at build time; official builds leave it unset → prod) and skips per-file signature timestamps. Recipients must clear quarantine once: `xattr -cr /Applications/Sim.app`.

The build also derives the app icon from `SIM_DESKTOP_DEFAULT_ORIGIN`. Every channel uses the exact production icon with its white background and black `sim` mark. Non-production channels add a thin outline using existing platform colors: dev uses orange, staging uses Loop blue, and localhost uses Workflow violet. The macOS menu-bar icon also carries a compact `D`, `S`, or `L` subscript for those environments; production remains unmarked. Packaged `.icns` files live in `build/`; `scripts/build.ts` copies the selected variant to the ignored `build/generated-icon.icns` path consumed by electron-builder. Matching 512px PNGs in `static/` provide the Dock icon for unpackaged runs.
The build also derives the app icon from `SIM_DESKTOP_DEFAULT_ORIGIN`. Every channel uses the exact production icon with its white background and black `sim` mark. Non-production channels add a thin outline using existing platform colors: dev uses orange, staging uses Loop blue, and localhost uses Workflow violet. The macOS menu-bar icon also carries a compact `D`, `S`, or `L` subscript for those environments; production remains unmarked. Native Icon Composer assets live in `build/`; `scripts/build.ts` copies the selected variant to the ignored `build/generated-icon.icon` path consumed by electron-builder. Electron-builder compiles it to `Assets.car` and derives the legacy `.icns` fallback from the same source. Matching 512px PNGs in `static/` provide the Dock icon for unpackaged runs.

CI (`.github/workflows/desktop-release.yml`, wired into `ci.yml`):
- Runs only after `create-release` on a `vX.Y.Z:` commit to main — **never before**: `scripts/create-single-release.ts` skips creation if the tag exists, so a desktop job publishing first would eat the changelog. The job builds `--publish never` and uploads assets with `gh release upload --clobber` (idempotent re-runs).
Expand Down
Binary file removed apps/desktop/build/icon-dev.icns
Binary file not shown.
Binary file removed apps/desktop/build/icon-local.icns
Binary file not shown.
3 changes: 3 additions & 0 deletions apps/desktop/build/icon-local.icon/Assets/border.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions apps/desktop/build/icon-local.icon/icon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"fill": {
"solid": "srgb:1.00000,1.00000,1.00000,1.00000"
},
"groups": [
{
"layers": [
{
"image-name": "logo.png",
"is-glass": false,
"name": "Sim"
},
{
"image-name": "border.svg",
"is-glass": false,
"name": "Local Border"
}
],
"shadow": {
"kind": "neutral",
"opacity": 0
},
"specular": false,
"translucency": {
"enabled": false,
"value": 0
}
}
],
"supported-platforms": {
"squares": ["macOS"]
}
}
Binary file removed apps/desktop/build/icon-staging.icns
Binary file not shown.
3 changes: 3 additions & 0 deletions apps/desktop/build/icon-staging.icon/Assets/border.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions apps/desktop/build/icon-staging.icon/icon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"fill": {
"solid": "srgb:1.00000,1.00000,1.00000,1.00000"
},
"groups": [
{
"layers": [
{
"image-name": "logo.png",
"is-glass": false,
"name": "Sim"
},
{
"image-name": "border.svg",
"is-glass": false,
"name": "Staging Border"
}
],
"shadow": {
"kind": "neutral",
"opacity": 0
},
"specular": false,
"translucency": {
"enabled": false,
"value": 0
}
}
],
"supported-platforms": {
"squares": ["macOS"]
}
}
Binary file removed apps/desktop/build/icon.icns
Binary file not shown.
Binary file added apps/desktop/build/icon.icon/Assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions apps/desktop/build/icon.icon/icon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"fill": {
"solid": "srgb:1.00000,1.00000,1.00000,1.00000"
},
"groups": [
{
"layers": [
{
"image-name": "logo.png",
"is-glass": false,
"name": "Sim"
}
],
"shadow": {
"kind": "neutral",
"opacity": 0
},
"specular": false,
"translucency": {
"enabled": false,
"value": 0
}
}
],
"supported-platforms": {
"squares": ["macOS"]
}
}
4 changes: 3 additions & 1 deletion apps/desktop/electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ mac:
arch: [universal]
- target: zip
arch: [universal]
icon: build/generated-icon.icns
# Compiles to Assets.car + CFBundleIconName for the native macOS icon path;
# electron-builder derives the legacy .icns fallback from the same source.
icon: build/generated-icon.icon
# node-pty keeps each architecture's binary at its own path, so both halves of
# the universal build carry an identical copy of both. @electron/universal
# refuses single-arch Mach-O files it wasn't told about, so name them here —
Expand Down
21 changes: 6 additions & 15 deletions apps/desktop/scripts/build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { copyFileSync } from 'node:fs'
import { cpSync, rmSync } from 'node:fs'
import { build } from 'esbuild'
import { identityForOrigin } from './channels'

const watch = process.argv.includes('--watch')

Expand All @@ -23,20 +24,10 @@ if (bakedDefaultOrigin) {
console.log(`• Baking default server origin: ${bakedDefaultOrigin}`)
}

/** Selects the branded app icon that matches the build's baked environment. */
function iconForOrigin(origin: string): string {
if (!origin) return 'build/icon.icns'
const host = new URL(origin).hostname.toLowerCase()
if (host === 'localhost' || host === '127.0.0.1') return 'build/icon-local.icns'
if (host === 'dev.sim.ai' || host.endsWith('.dev.sim.ai')) return 'build/icon-dev.icns'
if (host === 'staging.sim.ai' || host.endsWith('.staging.sim.ai')) {
return 'build/icon-staging.icns'
}
return 'build/icon.icns'
}

const appIcon = iconForOrigin(bakedDefaultOrigin)
copyFileSync(appIcon, 'build/generated-icon.icns')
const appIcon = identityForOrigin(bakedDefaultOrigin).icon
const generatedIcon = 'build/generated-icon.icon'
rmSync(generatedIcon, { force: true, recursive: true })
cpSync(appIcon, generatedIcon, { recursive: true })
console.log(`• Selecting desktop icon: ${appIcon}`)

const common = {
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/scripts/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface ChannelIdentity {
appId: string
/** Baked default origin + persisted settings origin. */
origin: string
/** Native Icon Composer source copied into the packager's generated path. */
icon: string
/**
* Artifact filename stem, and the per-channel scratch directory name.
* Space-free for the same reason electron-builder.yml's artifactName is:
Expand All @@ -36,24 +38,28 @@ export const PROD: ChannelIdentity = {
name: 'Sim',
appId: 'ai.sim.desktop',
origin: PROD_ORIGIN,
icon: 'build/icon.icon',
slug: 'sim',
}
export const STAGING: ChannelIdentity = {
name: 'Sim Staging',
appId: 'ai.sim.desktop.staging',
origin: STAGING_ORIGIN,
icon: 'build/icon-staging.icon',
slug: 'sim-staging',
}
export const DEV: ChannelIdentity = {
name: 'Sim Dev',
appId: 'ai.sim.desktop.dev',
origin: DEV_ORIGIN,
icon: 'build/icon-dev.icon',
slug: 'sim-dev',
}
export const LOCAL: ChannelIdentity = {
name: 'Sim Local',
appId: 'ai.sim.desktop.local',
origin: LOCAL_ORIGIN,
icon: 'build/icon-local.icon',
slug: 'sim-local',
}

Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/scripts/install-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ const identity = channelFlags.length === 1 ? CHANNEL_FLAGS[channelFlags[0]] : DE
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`)

Expand Down Expand Up @@ -121,6 +124,13 @@ function applyOrigin(origin: string): void {
}
}

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',
Expand Down Expand Up @@ -148,6 +158,7 @@ run('bunx', [
'-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)
Expand All @@ -169,6 +180,7 @@ 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)
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/scripts/package-share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* naming itself "Sim Dev" at runtime.
*
* Channels build ONE AT A TIME on purpose. scripts/build.ts writes the bundle
* to dist/ and the app icon to build/generated-icon.icns, both fixed paths, so
* to dist/ and the app icon to build/generated-icon.icon, both fixed paths, so
* concurrent channels would overwrite each other's bundle mid-package and ship
* a dmg whose baked origin belongs to a different environment — invisible until
* someone signs in. Giving each channel its own bundle directory is what would
Expand Down
Loading
Loading