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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,13 @@ of the transport layer knows or cares which one is in use.
| Windows | `DeepSQL Setup <version>.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 (&ldquo;DeepSQL&rdquo; 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

Expand Down
167 changes: 142 additions & 25 deletions src/pages/Download.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
}

Expand All @@ -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(), [])
Expand All @@ -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 })
})
Expand All @@ -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 (
<div className="min-h-screen bg-white text-gray-900">
<div className="max-w-3xl mx-auto px-6 py-16">
<Link
to="/login"
className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-800 transition-colors mb-8"
>
<ArrowLeft className="h-4 w-4" />
Back to sign in
</Link>

<header className="mb-12">
<div className="flex items-center gap-3 mb-4">
<div className="h-10 w-10 rounded-xl bg-gray-900 flex items-center justify-center">
Expand All @@ -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',
}}
/>
)}

Expand All @@ -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' && (
<>
<div className="flex items-baseline gap-3 mb-8 pb-4 border-b border-gray-200">
<span className="text-sm font-semibold text-gray-900">
{state.release.tag_name.replace(TAG_PREFIX, 'Version ')}
</span>
<div className="flex flex-wrap items-baseline gap-3 mb-6 pb-4 border-b border-gray-200">
<span className="text-sm font-semibold text-gray-900">{versionLabel}</span>
<span className="text-sm text-gray-400">
released {new Date(state.release.published_at).toLocaleDateString()}
</span>
</div>

{primary && (
<div className="mb-10">
<a
href={primary.browser_download_url}
className="inline-flex w-full sm:w-auto items-center justify-center gap-2 rounded-full bg-gray-900 px-6 py-3.5 text-sm font-semibold text-white shadow-lg transition-all hover:bg-gray-800 active:scale-[0.98]"
>
<DownloadIcon className="h-4 w-4" />
Download for {PLATFORMS[detected].label}
{primary.arch ? ` (${primary.arch})` : ''}
</a>
<p className="mt-2 text-xs text-gray-400">
{primary.kind} · {primary.name} · {formatSize(primary.size)}
</p>
</div>
)}

{detected === 'mac' && <MacGatekeeperNotice />}

{Object.entries(PLATFORMS).map(([key, meta]) => {
const assets = grouped[key] || []
if (!assets.length) return null
Expand All @@ -190,6 +264,13 @@ export default function Download() {
>
desktop/README.md
</a>
. All installers for this release are also on{' '}
<a
className="underline hover:text-gray-600 transition-colors"
href={state.release.html_url}
>
GitHub
</a>
.
</p>
</>
Expand Down Expand Up @@ -227,7 +308,9 @@ function PlatformSection({ platform, assets, highlight, showMacNote }) {
}`}
>
<span className="flex items-center gap-3 min-w-0">
<Package className={`h-4 w-4 shrink-0 ${highlight ? 'text-gray-300' : 'text-gray-400'}`} />
<Package
className={`h-4 w-4 shrink-0 ${highlight ? 'text-gray-300' : 'text-gray-400'}`}
/>
<span className="min-w-0">
<span className="block text-sm font-medium truncate">
{asset.kind}
Expand All @@ -247,18 +330,52 @@ function PlatformSection({ platform, assets, highlight, showMacNote }) {
))}
</div>

{showMacNote && (
<p className="mt-3 text-xs text-gray-400 leading-relaxed">
Builds are unsigned unless signing credentials are configured, so the first
launch needs <span className="text-gray-600">right-click → Open</span> (or{' '}
<code className="text-gray-600">xattr -dr com.apple.quarantine /Applications/DeepSQL.app</code>
).
</p>
)}
{showMacNote && <MacGatekeeperNotice compact />}
</section>
)
}

/** macOS builds ship unsigned unless signing secrets are configured in CI. */
function MacGatekeeperNotice({ compact = false }) {
return (
<div
className={`rounded-xl border border-amber-200 bg-amber-50 text-amber-950 ${
compact ? 'mt-3 px-4 py-3' : 'mb-10 px-5 py-4'
}`}
>
<div className="flex items-start gap-3">
<AlertTriangle className="h-4 w-4 text-amber-700 mt-0.5 shrink-0" />
<div className="text-sm leading-relaxed">
<p className={`font-semibold text-amber-950 ${compact ? 'mb-1' : 'mb-2'}`}>
macOS may block the first launch
</p>
<p className="text-amber-900/90 mb-3">
Installers are not Apple-notarized yet, so double-clicking can show
&ldquo;DeepSQL&rdquo; Not Opened with only <strong>Done</strong> and{' '}
<strong>Move to Trash</strong>. That is expected — use one of these instead:
</p>
<ol className="list-decimal pl-5 space-y-2 text-amber-900/90">
<li>
In Finder, <strong>right-click DeepSQL.app → Open</strong>, then click{' '}
<strong>Open</strong> in the dialog. You only need to do this once.
</li>
<li>
Or open <strong>System Settings → Privacy &amp; Security</strong>, scroll
down after the block, and click <strong>Open Anyway</strong>.
</li>
<li>
Or run in Terminal:{' '}
<code className="text-xs bg-amber-100/80 px-1.5 py-0.5 rounded">
xattr -dr com.apple.quarantine /Applications/DeepSQL.app
</code>
</li>
</ol>
</div>
</div>
</div>
)
}

function Notice({ tone, title, body, action }) {
return (
<div
Expand Down
19 changes: 16 additions & 3 deletions src/pages/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { useAuth } from '@/hooks/useAuth'
import { authAPI, setupAPI } from '@/lib/api/client'
import { ArrowLeft, Database, KeyRound, Mail, ShieldCheck, Sparkles, Zap, Activity, LineChart } from 'lucide-react'
import { ArrowLeft, Database, Download, KeyRound, Mail, ShieldCheck, Sparkles, Zap, Activity, LineChart } from 'lucide-react'

const STEP_LOGIN = 'login'
const STEP_OTP = 'otp'
Expand Down Expand Up @@ -303,6 +303,14 @@ export default function Login() {
</div>
))}
</div>

<Link
to="/download"
className="mt-10 inline-flex items-center gap-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors"
>
<Download className="w-4 h-4" />
Download DeepSQL Desktop
</Link>
</div>
</div>

Expand Down Expand Up @@ -352,8 +360,13 @@ export default function Login() {
)}
</div>

<div className="absolute bottom-6 left-0 right-0 text-center text-gray-500 text-sm px-6">
&copy; 2026 DeepSQL. Built for developers who love databases.
<div className="absolute bottom-6 left-0 right-0 text-center text-gray-500 text-sm px-6 space-y-1">
<p>
<Link to="/download" className="text-gray-600 hover:text-gray-900 underline underline-offset-2">
Download DeepSQL Desktop
</Link>
</p>
<p>&copy; 2026 DeepSQL. Built for developers who love databases.</p>
</div>
</div>
</div>
Expand Down
Loading