9 Commits
Author SHA1 Message Date
Michael ShimelesandClaude Opus 5.5 a12b5d14e4 feat(hl): built-in Turnkey wallets on testnet (#38)
* docs: design for Hyperliquid trading (crypto + HIP-3 stock perps)

Design only, no feature code. Covers custody (client-side API wallet
signing, recommended), agent approve/revoke flow, asset routing for
core/spot/HIP-3, order ticket, user streams for POS/ORD/ACTV, safety
rails, testnet-only test plan, phases, and open questions including the
Hyperliquid ToS restriction on Ontario.

* docs(hl-trading): plan the market key so HL perp rows reprice and trade

Greptile: market_key() returns None for Future and live_price/row
actions depend on it. Add Position.market and the portfolio_view work.

* docs(hl-trading): keep testnet and mainnet quotes in separate maps

Greptile: same market keys on both networks would let one overwrite the
other in the shared quote map. Plan a network-aware live_price and
mainnet-only gateway requests, with interleaved-update tests.

* feat(hl): add tr-hl, a testnet-only Hyperliquid signing and trading crate

tr-hl signs and sends Hyperliquid actions from the device. It covers
L1 phantom-agent signing and user-signed approveAgent, the order,
cancel, cancelByCloid, modify, updateLeverage, scheduleCancel and noop
actions, asset ids read from perpDexs and meta, tick and lot rounding,
info and exchange clients, a user-stream reducer and a nonce manager.

Only testnet exists in this crate. The endpoints are the testnet
hosts, the phantom agent source is always "b", hyperliquidChain is
always "Testnet", and Network has one variant. A test greps the crate
for mainnet hosts.

The caps are constants and the trader enforces them before it signs.
Orders are capped at $100 notional, leverage at 3x and market slippage
at 1%. The builder field is never sent. Signatures match the Python
SDK's published vectors, plus an approveAgent vector generated with
SDK 0.24.0. The crate builds for wasm32.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat: Hyperliquid testnet trading in the app

Hyperliquid joins LIVE next to SnapTrade, with its own opt-in in
Settings > Trading > Hyperliquid and an amber TESTNET badge on the
toolbar, the account, the ticket and every confirmation.

- Connect an address read-only. The account's positions, orders,
  fills and funding then show in PORT, POS, ORD and ACTV, from
  Hyperliquid's websocket and never through Terminal's server.
- Approve an agent on the desktop by pasting a throwaway testnet main
  key once. Terminal wipes it after signing and never stores it. In
  the browser the wallet signs instead (EIP-1193). The agent key lives
  in the macOS Keychain or the Linux Secret Service, and in browser
  memory or, by opt-in, IndexedDB under a non-extractable WebCrypto
  key. There is no file fallback.
- The ticket's Hyperliquid variant covers market (IOC, 1% cap), limit
  (GTC, IOC, post-only), stop loss and take profit, reduce-only and
  "Set 3x" leverage. HIP-3 rows read "PERP · HL xyz (perpetual, not a
  share)".
- Every order, cancel, cancel-all and leverage change asks for one
  confirmation. The worker re-reads the testnet mid and leverage and
  checks the caps again right before it signs.
- KILL and a button in ORD cancel every open order and turn
  Hyperliquid orders off. The dead man's switch is opt-in.
- Testnet rows price only from testnet mids. Position.market now
  carries the exact key, and live_price picks the map by network.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): mask addresses in Hyperliquid messages, toasts and logs

Hyperliquid's errors name full addresses ("User or API Wallet 0x… does
not exist", "Must deposit… User: 0x…"). mask_addresses turns each one
into 0x1234…abcd. Exchange summaries, the worker's logs and every app
message that shows a Hyperliquid error now go through it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): keep Hyperliquid out of SnapTrade's settings sections

The offscreen render showed the Hyperliquid account under SnapTrade's
Brokerage access, with SnapTrade's "reconnect with trading access" advice.
That section and the default-account picker now list SnapTrade accounts
only. Hyperliquid has its own section.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(hl-trading): record the v1 decisions and add screenshots and a wasm CI check

Section 15 of the design now records Micky's twelve decisions as
"Decided (2026-11-02)". The sections they change are updated: testnet
only, Secret Service with no file fallback on Linux, a pasted throwaway
key on the desktop for now, hard caps, an opt-in dead man's switch and
the kill switch. A new section 16 lists what v1 builds and what is still
missing.

docs/img/hl-*.png holds the before (origin/master) and after
screenshots. CI now builds and lints tr-hl for wasm32.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): read the agent key from the store once per account

When the store had no key for the approved agent, the app asked again
on every frame, which meant a Keychain or Secret Service read per frame.
It now remembers the account it read and asks again only after a
connect, an approval or a revoke.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): fix the kill switch, uncertain sends, approvals and cap valuation

Fixes from Greptile's first review of #37:

- Totals value a margined position (a perp) by its P&L, not its
  notional. Hyperliquid's cash is its collateral (equity less
  unrealised P&L), so a reprice moves equity only by the P&L change.
- KILL reads the open orders fresh on every dex, cancels them, and
  reads again (three rounds at most) before it reports what is left.
  Signed actions now run one at a time, and an order confirmed before
  a KILL that hasn't been signed yet is refused.
- An approval answer applies only to the account that asked for it,
  and Remove is disabled while one is in flight. The app also never
  signs with a key that isn't the approved agent's.
- An uncertain send blocks new orders until orderStatus finds its
  cloid, or until its 30 s expiresAfter window has passed without it.
- Every order needs a current mid before it is valued against the $100
  cap. Prices that round to zero are refused, and rounding zero no
  longer overflows.
- The keyring check runs in the worker, not on the UI thread.
- A cancel-all skips a market the registry doesn't know instead of
  failing, and the pasted key field's undo history is wiped once the
  key has signed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): keep the dead man's switch armed while an order is unresolved

From Greptile's second review of #37: an order with no clear answer
blocked new sends through the same guard the dead man's switch reads,
so the switch cleared its scheduled cancel just when a possibly
accepted order needed it.

The guard is now split. Whether this device can sign arms the switch,
and allows cancels and leverage changes. Only new orders and modifies
also wait for an unresolved order. KILL always goes ahead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): size dollar amounts at the mid and let KILL skip stalled requests

Fixes from Greptile's fourth review of #37 (at f636b7c):

- A dollar amount on the Hyperliquid ticket (BUY xyz:INTC $10) became
  that many contracts. The ticket now sizes it at the testnet mid,
  rounded down to the market's size step. With no mid yet, or under one
  step, it stays a dollar amount and the ticket can't send it. Changing
  the market sizes the amount again.
- Every /info and /exchange request has its own 10 s deadline. The
  browser's fetch had none, so a stalled request could hold the
  worker's only turn forever.
- KILL no longer waits for that turn. It cancels at once, and an order
  confirmed before it gives up, even one still reading the market. If
  an order was already being sent, KILL cancels again once its answer
  comes, or once its expiresAfter has passed if the send got no clear
  answer. Single cancels and the dead man's switch don't wait for
  orders either. Orders, modifies and leverage changes still go one at
  a time, and an order with no clear answer still blocks new ones until
  it is found or expires.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): size dollars for the market named now and keep timed cancels in order

Fixes from Greptile's review of f1ff1b3:

- The ticket reads its market at the start of a frame, before an edit
  to the market field. A dollar amount could then be sized at the old
  market's mid ($10 at a $2 mid is 5 units, worth $20 on a $4 market).
  settle_size now sizes only with the market the ticket names, so the
  next frame sizes it for the new one. The ticket's estimate also uses
  that market.
- Dead man's switch changes ran side by side, so a slow clear could
  land after a newer schedule and remove it. They now go out one at a
  time in the order the app made them, on a lock of their own that
  never waits for orders. A change that reaches the worker after a
  newer one is dropped. After a change with no clear answer, the next
  waits until it can no longer land.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): give each dead man's switch refresh a fresh deadline

From Greptile's review of 2f7cab6: after a change with no clear answer,
every later change waited until that one could no longer land. Refreshes
queue every 30 s, so with lost replies they went out every 35 s or so,
each with the deadline it got when queued, and the timer could fire
while Terminal was still running.

- ScheduleCancel now carries how far ahead to cancel. The worker adds
  it to the time of signing, so a refresh that waited still gets a
  fresh deadline.
- A newer change replaces one still waiting for its turn, so a backlog
  collapses into the newest.
- Only a change of the other kind waits out a lost one: a timer after a
  clear, or a clear after a timer. A refresh that lands late only moves
  the timer back by one refresh, so refreshes don't wait for each other.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): make every dead man's switch change wait out a lost one

From Greptile's review of 73b71b8: letting a refresh go out before an
earlier timer with a lost reply could no longer land was not safe. That
timer could land last (the clocks may differ by seconds) and pull the
deadline in, so the switch could fire while Terminal was running.

Every change now waits until a lost earlier change can no longer land,
whichever kind either one is. That is safe now that each change is
signed, with its deadline, when it goes out, and a newer change
replaces any still waiting, so a backlog can't build up stale
deadlines.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(hl): sign approveAgent through a built-in Turnkey wallet, testnet only

Add tr-turnkey, the client side of Terminal's built-in wallets. The device
makes a P-256 key, and every Turnkey request carries a stamp made with it.
The session token Turnkey issues for that key lasts 15 minutes, and Terminal
accepts it only if it is bound to its own key. The client reads whoami and
the wallet's Ethereum account, and signs one digest with the request
alloy-signer-turnkey sends (SIGN_RAW_PAYLOAD_V2, hex payload, no hashing).
On the desktop the browser returns the session to a listener on 127.0.0.1
that checks a 256-bit state. A mock feature adds a local stand-in for
Turnkey's API and sign-in page for tests and screenshots.

tr-hl gets sign_approve_agent_with, the only way a remote wallet's
signature enters the crate. The signer sees only the digest of a testnet
approveAgent, and the answer is used only if it recovers the wallet.
approve_agent_hash now also refuses every signatureChainId except 0x66eee.

The mock wallet holds the Python SDK's public test key and reproduces the
SDK's approveAgent vector. Mainnet and 0xa4b1 approvals never reach the
wallet, and the new code names no mainnet host.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(hl): design built-in Turnkey wallets for testnet trading

Record Micky's decision of 2026-11-04 in §15: built-in wallets only, no
browser wallet and no pasted key. The new §4.5 covers who holds which key,
the browser sign-in that hands a session to a key made on the device, why
a loopback redirect beats a terminal:// link, what Turnkey signs and how
often, and the export through Turnkey's iframe. It explains why no
Terminal server needs a Turnkey key (the Auth Proxy holds the one that
creates sub-organizations) and sketches a scoped signup function as an
unbuilt fallback. Agents now last 180 days by default, since each approval
is a paid Turnkey signature.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(hl): replace the browser wallet and pasted key with the built-in wallet

Settings › Trading › Hyperliquid now starts with "Create Wallet / Sign In".
On the desktop it opens Terminal's sign-in page in the system browser and
waits on a 127.0.0.1 loopback for the session; the web build opens the page
in a popup and takes its postMessage. Terminal reads the wallet's address
from Turnkey with requests stamped by its own key, keeps the 15-minute
session in the OS secret store (memory on the web) and deletes it once it
expires.

"Approve Trading Key" makes the agent key on the device and has the wallet
sign approveAgent through Turnkey, signing in first if there is no session.
Revoke goes the same way. Agents now last 180 days by default, since each
approval is a paid Turnkey signature. Orders are still signed only by the
agent key.

Deposit shows the address in full with a QR code and a copy button, the
testnet perp account value and withdrawable from the stream, and spot USDC
(new spotClearinghouseState read). Back Up Wallet warns, then opens the
page's export mode.

The web's EIP-1193 Connect Wallet and the desktop's pasted testnet key are
gone. MainKey is left for tests and the testnet_check example. Watching an
address read-only stays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(site): add the wallet sign-in and backup page

/wallet/ signs the user in with an email code or a passkey through
@turnkey/core and Turnkey's Auth Proxy. A new user gets a sub-organization
with one Ethereum wallet. The page then calls stamp_login for the app's
public key, so the app gets a 15-minute session only its own key can use,
and logs itself out. Desktop answers go to the loopback port, web answers
to the opener with postMessage on the same origin.

Its parameters come from the URL fragment, which never reaches a server,
analytics or a Referer, and the page clears it on load. In export mode
the page shows the recovery phrase in Turnkey's export iframe, so the
phrase never touches the page's code or any server.

It needs two public IDs in the Vercel project env and says so until they
are set. @turnkey/core is pinned to 2.13.1 and @turnkey/iframe-stamper to
2.11.1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci: build and lint tr-turnkey for wasm32

The browser build uses the wallet crate too, so check it the way tr-hl is
checked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): keep the trading key when the wallet doesn't sign a revoke

Revoke used to forget the agent before Turnkey signed anything, so a failed
or expired sign-in left the app without its agent while the message said
nothing changed. Orders now turn off at once, and the agent is forgotten
only when the revoke goes through. An approval the wallet didn't sign now
says so instead of reading as Hyperliquid's refusal. The mock-URL warnings
no longer print the URL.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): link the sign-in page at /wallet, its canonical URL

The site serves the page at /wallet and redirects /wallet/ there. Browsers
keep the fragment across that redirect, but there is no reason to rely on
it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(site): only sign in, never sign up, when backing up a wallet

Export mode hid nothing: it offered "Create with a passkey", and the email
code path would create a new wallet for an unknown address. Backing up now
logs in to an existing wallet only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: add before and after screenshots for built-in wallets

Desktop shots are offscreen renders of the running app against a local
Turnkey stand-in. Web shots come from headless Chromium on a local build.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(hl): keep sign-in answers and wallet actions on the right wallet

From Greptile on #38:

- Removing the wallet reset the request counter, so a cancelled sign-in's
  late answer could carry the next sign-in's id and end it. Ids now keep
  counting, and removing the wallet cancels a sign-in in progress.
- A revoke or approval that needed a sign-in acted on whichever wallet the
  user signed in to. The sign-in now remembers the wallet the action is
  for, and refuses another one without changing anything.
- The browser build's sign-in never timed out. Both builds now give up
  after 10 minutes, the limit the waiting line shows.
- The sign-in page stayed on "Connecting your wallet" if Turnkey didn't
  issue the device's session. It now goes back to the sign-in controls
  with the error, so the user can retry or cancel.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-04 08:38:08 -05:00
Michael ShimelesandClaude Opus 5.5 7d999a8e19 Rename the project from terminal-rs to terminal (#27)
* chore: rename the project from terminal-rs to terminal

The crate and binary are now `terminal`, and user-facing text, docs, CI,
scripts and repo URLs drop the -rs.

Saved data follows the new name:
- The desktop data folder moves from terminal-rs to terminal on first
  launch, and a link at the old path keeps older builds working.
- The browser build copies localStorage keys from terminal-rs/ to
  terminal/ once and never overwrites newer keys.
- SnapTrade credentials move from Keychain service terminal-rs to
  terminal the first time the app reads them.

The bundle ID (dev.terminal-rs.app), the Sparkle signing key's keychain
account and the notary profile keep the old name, because released
copies and the release Mac depend on them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: add screenshots for the rename PR (removed in the next commit)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: drop the PR screenshots so they stay out of master

The PR body links them at the previous commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(storage): keep fallback saves and the old-folder link working

A browser tab that couldn't copy its keys keeps the old prefix. If
another tab later finishes the copy, that tab's saves went to keys the
next load skips. Its paths under terminal-rs/ now map to terminal/ as
soon as the copy's marker exists.

The link left at the old folder repeated `new` as given, so a relative
HOME or XDG_DATA_HOME made it dangle. It now names its sibling.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-29 10:47:58 -04:00
Michael ShimelesandClaude Opus 5.5 fa1b3c8886 fix(web): cache the app's wasm for good and serve /app/ without a redirect chain (#26)
* fix(web): cache the app's wasm for good and serve /app/ without a redirect chain

scripts/build-web.sh now writes each build's JS and wasm to
static/app/immutable/<hash>/, named after a hash of those files, and
fills the paths into index.html. vercel.json caches /app/immutable/ for
a year and answers a missing file there with a 404 marked no-store.
index.html and the fonts keep Vercel's default of revalidating on every
visit, so a deploy can't leave a page with one build's JS and another
build's wasm.

/app/ used to take two 308s through the SvelteKit function to reach the
page (/app/ to ../app to /app/index.html). vercel.json now sends /app
to /app/ with one 308 and serves /app/ from index.html, so the /app
endpoint is gone. Relative URLs in the page still resolve under /app/.

Both workflows move off the Node 20 majors: actions/checkout@v7,
upload-artifact@v7, download-artifact@v8 and setup-node@v7 run on
Node 24.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(web): fall back to shasum where sha256sum is missing

Older macOS releases ship shasum but not sha256sum, so build-web.sh
stopped with exit 127 before writing index.html. Both commands give the
same digest, so the folder names don't change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-29 09:07:17 -04:00
Michael ShimelesandClaude Opus 5.5 bf1dd94fd5 ci(web): deploy the site with the browser build from GitHub Actions (#24)
Vercel's build doesn't run Rust, so a deploy from Vercel's Git integration
has no /app. The new deploy-site workflow runs scripts/build-web.sh, then
vercel build and vercel deploy --prebuilt. Pushes to master deploy to
production, and pull requests from this repository get a preview.
site/vercel.json turns off Vercel's Git deployments so each commit deploys
once.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-29 07:47:03 -04:00
Michael ShimelesandCursor 2ae060d2d0 fix(macos): ship as Terminal.dmg with the logo as its volume icon (#22)
* fix(macos): ship as Terminal.dmg with the logo as its volume icon

Bumps to 0.1.1 so the next release replaces the v0.1.0 DMG, which predates the Terminal name and icon, and renames the remaining user-facing terminal-rs strings.

Co-authored-by: Cursor <[email protected]>

* fix(macos): detach the DMG's writable image if a release build fails while it's mounted

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
2026-10-20 22:16:52 -04:00
Michael ShimelesandCursor f69f864d78 feat(site): ask for an email before the download starts (#21)
* feat(site): ask for an email before the download starts

The download buttons now open a dialog that asks for an email. It posts the
address to `POST /download`, which adds it to an `emails` sorted set in
Upstash Redis scored by when it was first given, then starts the download
with `GET /download`. The browser remembers that it gave an email, so later
downloads skip the dialog.

A failed save still downloads, and direct `/download` links and visitors
without JavaScript download without being asked. The Redis credentials are
optional in `src/env.ts`, so the site builds and serves downloads before the
Upstash integration is connected.

Co-authored-by: Cursor <[email protected]>

* fix(site): start the download if saving the email takes over 3 seconds

The dialog waited for `POST /download` to settle before starting the
download, so a hung Redis save left the button disabled with nothing
downloading. Abort the save after 3 seconds and take the existing
download-anyway path, without remembering the email as given.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
2026-10-20 10:42:12 -04:00
Michael ShimelesandCursor 9dba5760d9 Rebuild the site around an animated demo with sound (#12)
* Rebuild the site around an animated demo with sound

The page now opens with a 20-second demo of the app. AAPL is typed and
loads a chart, 5 switches it to five-minute bars, NVDA OMON opens the
option chain, and a Congress alert arrives. Captions follow each step.
"Play with sound" replays it once with sounds synthesized in the browser,
so there are no audio files. Six feature cards loop small animations of
the chart, world markets, the command palette, the Congress feed, the
portfolio and the themes, each only while it is on screen.

The design follows the Terminal landing in Paper: dark only, SF fonts,
glass controls and the new T icon, with no orange gradients. The page
now hydrates because the demo needs JavaScript. With reduced motion the
demo and cards show one still frame and nothing fades in on scroll.

Co-authored-by: Cursor <[email protected]>

* Use the candlestick logo

Swap the T monogram for the three rising candlesticks from the Paper logo options, in the page logo and in assets-src/icon.svg, and regenerate the icons and share card.

Co-authored-by: Cursor <[email protected]>

* Move the theme switch into the nav as an icon button

The button now switches the whole page, not just the demo. The page,
the demo and the feature cards all have light styles, the choice is
saved, and app.html applies it before the first paint. The demo
captures are CSS backgrounds so only the current theme's image loads.

Co-authored-by: Cursor <[email protected]>

* Remove the build-from-source block from the Install section

Co-authored-by: Cursor <[email protected]>

* Show revealed sections when reduced motion turns on, and name the chapter buttons

Turning on reduced motion reran the reveal attachment, which disconnected the observer and returned early, leaving unread sections at opacity 0. Teardown now marks the element shown. The chapter buttons lose their only text at 640px and below, so they get an aria-label.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
2026-10-19 16:22:17 -04:00
Michael ShimelesandCursor cdc4c585f6 Rename the site to Terminal and add full share metadata (#11)
* Rename the site to Terminal and fill in its share metadata

The page still called the app terminal-rs and pointed its canonical URL,
og:url and og:image at terminal-rs.vercel.app, while the site now lives at
terminal.rasmic.xyz. Move SITE_URL to the custom domain, build every
absolute URL from it, and show the name as Terminal in the title, alt text,
Open Graph and Twitter tags and the share card. The tagline stays next to
the name so it still reads as a market terminal, not Apple's Terminal.

Fill the gaps link previews and crawlers look for: og:site_name, og:locale,
og:image:type, twitter:image:alt, light and dark theme-color, a
SoftwareApplication JSON-LD block, and a sitemap that robots.txt points to.
The description is trimmed to 157 characters so search results don't cut
it off.

og.jpg is re-rendered from assets-src/og.html with scripts/build-images.sh;
the other generated images came out byte-identical. The GitHub repo, DMG
and package names keep -rs.

Co-authored-by: Cursor <[email protected]>

* Make theme-color follow the theme the page shows

The two theme-color tags followed the system appearance through their
media queries, but the toggle lets you pick and save the other theme, so
choosing light on a dark Mac left the browser chrome dark, before and
after a reload.

Move the tag into app.html ahead of the theme script and let apply() set
it with the page's data-theme. It now matches on first paint, after a
saved choice is restored, on every toggle and when the system appearance
changes. The page ships no framework JavaScript (csr = false), so nothing
rewrites the tag later. Without JavaScript the page is always dark, and
the tag's static value is the dark background too.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
2026-10-19 12:56:51 -04:00
Michael ShimelesandCursor 2289312ff9 Add the download page under site/ (#9)
* Add the download page under site/

A one-page SvelteKit site for Vercel with a /download route that
redirects to the latest release DMG. It is its own npm project, apart
from the Cargo workspace; deploy it with the Vercel Root Directory set
to site.

Co-authored-by: Cursor <[email protected]>

* Remove the amber glow behind the hero

The backdrop keeps its grid and fade. Drop the --glow colour too,
since nothing else used it.

Co-authored-by: Cursor <[email protected]>

* Document the site's Node minimum and find Chrome on Linux

SvelteKit needs Node 22.17 or later, and engine-strict makes npm
install stop on older versions, so declare it in engines and say so
in the README. build-images.sh now finds Chrome or Chromium on PATH
before falling back to the macOS app, and stops with a clear message
when neither is there.

Co-authored-by: Cursor <[email protected]>

* Let CHROME name a command on PATH

The executable check added in 875b484 only accepted paths, so an override like
CHROME=google-chrome-stable stopped working. Resolve the override with
command -v, the same way discovery resolves its candidates.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
2026-10-18 15:52:50 -04:00