diff --git a/README.md b/README.md index d22ba99..7cb4ab4 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,13 @@ your schema — all from one shared brain. ### DeepSQL Desktop (optional) A thin Electron client for a self-hosted VM — direct TLS or an in-process SSH tunnel — -without bundling a second copy of the web UI. Separate npm project: +without bundling a second copy of the web UI. + +**Download installers** (macOS, Windows, Linux) from the public page at `/download` on +your DeepSQL instance, or browse +[GitHub Releases](https://github.com/DeepSQLAI/deepsql/releases) for `desktop-v*` tags. + +To build from source (separate npm project): ```bash cd desktop diff --git a/desktop/README.md b/desktop/README.md index dd012b6..9e40c0d 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -53,9 +53,13 @@ of the transport layer knows or cares which one is in use. | Windows | `DeepSQL Setup .exe` (NSIS), portable `.exe` | | Linux | `.AppImage`, `.deb`, `.rpm` | -macOS builds are unsigned unless you supply signing credentials, so the first -launch needs **right-click → Open** (or `xattr -dr com.apple.quarantine -/Applications/DeepSQL.app`). +macOS builds are unsigned unless you supply signing credentials, so Gatekeeper +blocks a normal double-click on first launch (“DeepSQL” Not Opened). +Use one of: + +1. **Right-click → Open** on `DeepSQL.app`, then click **Open** in the dialog. +2. **System Settings → Privacy & Security → Open Anyway** after the block. +3. Terminal: `xattr -dr com.apple.quarantine /Applications/DeepSQL.app` ### From source diff --git a/src/pages/Download.jsx b/src/pages/Download.jsx index ca8469b..0b90f0b 100644 --- a/src/pages/Download.jsx +++ b/src/pages/Download.jsx @@ -1,7 +1,9 @@ import { useEffect, useMemo, useState } from 'react' +import { Link } from 'react-router-dom' import { AlertTriangle, Apple, + ArrowLeft, Download as DownloadIcon, Loader2, Monitor, @@ -42,17 +44,18 @@ function classify(asset) { ? 'Intel / AMD64' : null - if (name.endsWith('.dmg')) return { platform: 'mac', kind: 'Disk image', arch } - if (name.endsWith('.zip')) return { platform: 'mac', kind: 'Zip archive', arch } + if (name.endsWith('.dmg')) return { platform: 'mac', kind: 'Disk image', arch, rank: 0 } + if (name.endsWith('.zip')) return { platform: 'mac', kind: 'Zip archive', arch, rank: 1 } if (name.endsWith('.exe')) return { platform: 'windows', kind: name.includes('setup') ? 'Installer' : 'Portable', arch, + rank: name.includes('setup') ? 0 : 1, } - if (name.endsWith('.appimage')) return { platform: 'linux', kind: 'AppImage', arch } - if (name.endsWith('.deb')) return { platform: 'linux', kind: 'Debian package', arch } - if (name.endsWith('.rpm')) return { platform: 'linux', kind: 'RPM package', arch } + if (name.endsWith('.appimage')) return { platform: 'linux', kind: 'AppImage', arch, rank: 1 } + if (name.endsWith('.deb')) return { platform: 'linux', kind: 'Debian package', arch, rank: 0 } + if (name.endsWith('.rpm')) return { platform: 'linux', kind: 'RPM package', arch, rank: 2 } return null } @@ -65,12 +68,44 @@ function detectPlatform() { return null } +/** Prefer Apple Silicon builds on arm64 Macs when both exist. */ +function detectMacArchPreference() { + const ua = navigator.userAgent || '' + if (/arm64|aarch64/i.test(ua)) return 'Apple Silicon' + return 'Intel / AMD64' +} + function formatSize(bytes) { if (!bytes) return '' const mb = bytes / (1024 * 1024) return `${mb.toFixed(1)} MB` } +function pickPrimaryAsset(grouped, platform) { + const assets = grouped[platform] || [] + if (!assets.length) return null + + const archPref = platform === 'mac' ? detectMacArchPreference() : null + const sorted = [...assets].sort((a, b) => { + if (archPref) { + const aMatch = a.arch === archPref ? 0 : 1 + const bMatch = b.arch === archPref ? 0 : 1 + if (aMatch !== bMatch) return aMatch - bMatch + } + return (a.rank ?? 9) - (b.rank ?? 9) + }) + return sorted[0] +} + +function latestDesktopRelease(releases) { + return releases + .filter((r) => r.tag_name?.startsWith(TAG_PREFIX) && !r.draft) + .sort( + (a, b) => + new Date(b.published_at).getTime() - new Date(a.published_at).getTime(), + )[0] +} + export default function Download() { const [state, setState] = useState({ status: 'loading' }) const detected = useMemo(() => detectPlatform(), []) @@ -87,11 +122,7 @@ export default function Download() { }) .then((releases) => { if (cancelled) return - const release = releases.find( - (r) => r.tag_name?.startsWith(TAG_PREFIX) && !r.draft, - ) - // No desktop release yet is a *different* answer from "we could not - // check", and the page must not blur the two into one empty state. + const release = latestDesktopRelease(releases) if (!release) return setState({ status: 'none' }) setState({ status: 'ready', release }) }) @@ -112,12 +143,33 @@ export default function Download() { const meta = classify(asset) if (meta) out[meta.platform].push({ ...asset, ...meta }) } + for (const key of Object.keys(out)) { + out[key].sort((a, b) => (a.rank ?? 9) - (b.rank ?? 9)) + } return out }, [state]) + const primary = useMemo( + () => (detected ? pickPrimaryAsset(grouped, detected) : null), + [grouped, detected], + ) + + const versionLabel = + state.status === 'ready' + ? state.release.tag_name.replace(TAG_PREFIX, 'Version ') + : null + return (
+ + + Back to sign in + +
@@ -144,7 +196,10 @@ export default function Download() { tone="error" title="Could not reach GitHub" body={`The download list could not be loaded (${state.message}). This is a problem fetching the release list, not a sign that no build exists — you can browse releases directly on GitHub.`} - action={{ href: `https://github.com/${REPO}/releases`, label: 'Open releases on GitHub' }} + action={{ + href: `https://github.com/${REPO}/releases`, + label: 'Open releases on GitHub', + }} /> )} @@ -153,21 +208,40 @@ export default function Download() { tone="info" title="No desktop build published yet" body="The release list loaded fine — there is simply no desktop-v* release with attached installers. Builds are produced by the desktop-release workflow when a desktop-v* tag is pushed." - action={{ href: `https://github.com/${REPO}/releases`, label: 'Open releases on GitHub' }} + action={{ + href: `https://github.com/${REPO}/releases`, + label: 'Open releases on GitHub', + }} /> )} {state.status === 'ready' && ( <> -
- - {state.release.tag_name.replace(TAG_PREFIX, 'Version ')} - +
+ {versionLabel} released {new Date(state.release.published_at).toLocaleDateString()}
+ {primary && ( +
+ + + Download for {PLATFORMS[detected].label} + {primary.arch ? ` (${primary.arch})` : ''} + +

+ {primary.kind} · {primary.name} · {formatSize(primary.size)} +

+
+ )} + + {detected === 'mac' && } + {Object.entries(PLATFORMS).map(([key, meta]) => { const assets = grouped[key] || [] if (!assets.length) return null @@ -190,6 +264,13 @@ export default function Download() { > desktop/README.md + . All installers for this release are also on{' '} + + GitHub + .

@@ -227,7 +308,9 @@ function PlatformSection({ platform, assets, highlight, showMacNote }) { }`} > - + {asset.kind} @@ -247,18 +330,52 @@ function PlatformSection({ platform, assets, highlight, showMacNote }) { ))}
- {showMacNote && ( -

- Builds are unsigned unless signing credentials are configured, so the first - launch needs right-click → Open (or{' '} - xattr -dr com.apple.quarantine /Applications/DeepSQL.app - ). -

- )} + {showMacNote && } ) } +/** macOS builds ship unsigned unless signing secrets are configured in CI. */ +function MacGatekeeperNotice({ compact = false }) { + return ( +
+
+ +
+

+ macOS may block the first launch +

+

+ Installers are not Apple-notarized yet, so double-clicking can show + “DeepSQL” Not Opened with only Done and{' '} + Move to Trash. That is expected — use one of these instead: +

+
    +
  1. + In Finder, right-click DeepSQL.app → Open, then click{' '} + Open in the dialog. You only need to do this once. +
  2. +
  3. + Or open System Settings → Privacy & Security, scroll + down after the block, and click Open Anyway. +
  4. +
  5. + Or run in Terminal:{' '} + + xattr -dr com.apple.quarantine /Applications/DeepSQL.app + +
  6. +
+
+
+
+ ) +} + function Notice({ tone, title, body, action }) { return (
))}
+ + + + Download DeepSQL Desktop +
@@ -352,8 +360,13 @@ export default function Login() { )}
-
- © 2026 DeepSQL. Built for developers who love databases. +
+

+ + Download DeepSQL Desktop + +

+

© 2026 DeepSQL. Built for developers who love databases.