From 7cf8567e9120867f7c45a0db6a8fc4a4a5e2c04c Mon Sep 17 00:00:00 2001 From: Lucas Doell Date: Fri, 2 Oct 2026 18:22:33 -0400 Subject: [PATCH 01/20] docs: glossary for Releases and Updates, ADR on update-check install IDs --- CONTEXT.md | 12 ++++++++++++ ...17-update-checks-carry-an-anonymous-install-id.md | 8 ++++++++ 2 files changed, 20 insertions(+) create mode 100644 docs/adr/0017-update-checks-carry-an-anonymous-install-id.md diff --git a/CONTEXT.md b/CONTEXT.md index dfa132c2..12162662 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -43,6 +43,18 @@ _Avoid_: Error, disconnected **Offline**: The Connection State of a Host the Client has stopped retrying, such as a machine that is shut down. +**Release**: +A published version of Polaris: one version number shared by the Desktop App and the Daemon builds it carries. +_Avoid_: Build (for a published version), drop + +**Update**: +The Desktop App replacing itself with a newer Release. +_Avoid_: Upgrade (for the Desktop App) + +**Daemon Upgrade**: +A Host's Daemon replaced by the build the Desktop App carries. +_Avoid_: Update (for a Daemon), reinstall + ### Agents **Harness**: diff --git a/docs/adr/0017-update-checks-carry-an-anonymous-install-id.md b/docs/adr/0017-update-checks-carry-an-anonymous-install-id.md new file mode 100644 index 00000000..0d48f922 --- /dev/null +++ b/docs/adr/0017-update-checks-carry-an-anonymous-install-id.md @@ -0,0 +1,8 @@ +# Update checks carry an anonymous install ID + +The Desktop App asks Polaris's own update feed (a route on the marketing site, recorded in Axiom) rather than `update.electronjs.org`, and each check sends a random install ID made on first launch along with the version, arch and macOS version. That ID is the only way to count active installs and how fast a Release is adopted, and it is tied to nothing else: no account, Host, Workspace or IP (the country is derived, then the IP dropped). Turning off "Check for updates automatically" in Settings stops the checks and the ID with them. What Polaris sends is listed in Settings → About and on the site. Wider product telemetry (OTel) will be a separate opt-out, not folded into this. + +## Considered Options + +- `update.electronjs.org`: no code to run, but no analytics and no control over what is offered. +- Request-level counts only (no ID): downloads and the version spread, but not active installs. From 863e7bfb55d4d51659021b5041f12bbc740dc0f2 Mon Sep 17 00:00:00 2001 From: Lucas Doell Date: Fri, 2 Oct 2026 19:02:12 -0400 Subject: [PATCH 02/20] docs(design): Installer section for the DMG window (ENG-255) --- DESIGN.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/DESIGN.md b/DESIGN.md index 45a90d90..8d018c5f 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -312,6 +312,16 @@ The Desktop App is Electron (Chromium 152, React). CSS effects are available: sh - Draw the mark on whole pixels only; never rotate, outline, or anti-alias it. - **Marketing site:** the "Marketing site" page of the Paper file (S1, desktop, dark, a draft). It uses the brand deck's system: Night ground, the night-sky scene in the hero and the closing CTA only, SF Pro Regular headlines with tight tracking, mono section kickers (`01 — Supervise`), and real product shots exported from accepted artboards (`design/assets/site/`), never redrawn UI. One primary action on every screen, "Download for macOS", as a Snow button; the secondary action is always the source. Starlight stays off buttons and links here too (rule/starlight-is-rare), and signal colours appear only inside product shots. Until the repo is public, every source link reads "Source opens soon" and leads to a one-email notify form. Copy names what Polaris does, not the hardware it runs on: hosts are "this Mac" or "any machine you can reach over SSH". The footer credits lux.dev LLC ("An open source project by lux.dev", "© 2026 lux.dev LLC"). rule/scene-text-contrast holds on the site too: the nav sits on a solid Night bar (~95%) with links in `#C9D0E0`, never directly on the sky, and buttons over a scene are opaque. S2 is the light version: the dawn scene in the hero (faded into `bg-light` above the product shot) and the closing CTA, light product shots, ink primary buttons, the blue colourway of the mark, and light dither and wash variants. S3 is mobile (390, dark): one column, shots bleed off the right edge, and because a phone cannot install a Mac app the primary action becomes "Email me the Mac download" (email field plus button) instead of Download. +### Installer + +The DMG window: the "Installer" page of the Paper file (I1 is the candidate; I2 is the light-appearance check). Drag the app to Applications under the night sky; no wordmark and no copy. + +- **Window:** 660×400 points of content (the background size; Finder's title bar sits above it), toolbar, sidebar, path bar and status bar hidden, icon view, icons 100pt, labels 12pt at the bottom, no item info. The volume is named "Polaris" and its volume icon is `Polaris.icns`. +- **Icon centres** (points from the content's top-left, as `.DS_Store` stores them): Polaris.app at **(165, 190)**, the Applications alias at **(495, 190)**. Both labels end above the first hill line. +- **Background:** the night scene drawn natively at 330×200 (`scene()` in `gen_textures.py` with the night palettes, hills and seed 11, the cabin seated by `place_cabin` in the same 0.14–0.34 band), scaled by nearest neighbour ×2 for 1× (660×400) and ×4 for 2× (1320×800), so one scene pixel is 2pt. Polaris lands at (474, 72), in the sky above Applications; the cabin sits under the app. Stars inside each icon and label box (centre −29…+29 cells across, −29…+40 cells down) and around the arrow are put back to the sky beneath them, so nothing twinkles through an icon or a label. +- **Arrow:** centred between the icons at scene cell (165, 95), 48 cells long, in `#C9D0E0` (never Starlight, rule/starlight-is-rare): a one-cell dashed shaft (2 on, 1 off) into an open chevron head whose arms step five cells back in a two-cell stair. Drawn into the background on whole scene pixels; never anti-aliased. +- **Open decision:** Finder draws icon labels in the system label colour, white in dark appearance and black in light (I2, inferred from Finder's behaviour; the build task screenshots both). On the night sky the black labels fail rule/scene-text-contrast. Until the user decides, the dark design stands. + ## Colors ### Primary From 6a1dbfda3289397a0c07b304f74ca758ec00669b Mon Sep 17 00:00:00 2001 From: Lucas Doell Date: Fri, 2 Oct 2026 19:07:09 -0400 Subject: [PATCH 03/20] docs(design): record the Installer's light-appearance label trade-off (ENG-255) --- DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESIGN.md b/DESIGN.md index 8d018c5f..cf90eac0 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -320,7 +320,7 @@ The DMG window: the "Installer" page of the Paper file (I1 is the candidate; I2 - **Icon centres** (points from the content's top-left, as `.DS_Store` stores them): Polaris.app at **(165, 190)**, the Applications alias at **(495, 190)**. Both labels end above the first hill line. - **Background:** the night scene drawn natively at 330×200 (`scene()` in `gen_textures.py` with the night palettes, hills and seed 11, the cabin seated by `place_cabin` in the same 0.14–0.34 band), scaled by nearest neighbour ×2 for 1× (660×400) and ×4 for 2× (1320×800), so one scene pixel is 2pt. Polaris lands at (474, 72), in the sky above Applications; the cabin sits under the app. Stars inside each icon and label box (centre −29…+29 cells across, −29…+40 cells down) and around the arrow are put back to the sky beneath them, so nothing twinkles through an icon or a label. - **Arrow:** centred between the icons at scene cell (165, 95), 48 cells long, in `#C9D0E0` (never Starlight, rule/starlight-is-rare): a one-cell dashed shaft (2 on, 1 off) into an open chevron head whose arms step five cells back in a two-cell stair. Drawn into the background on whole scene pixels; never anti-aliased. -- **Open decision:** Finder draws icon labels in the system label colour, white in dark appearance and black in light (I2, inferred from Finder's behaviour; the build task screenshots both). On the night sky the black labels fail rule/scene-text-contrast. Until the user decides, the dark design stands. +- **Labels in light appearance (accepted trade-off):** Finder draws icon labels white in dark appearance and black in light, so in light the labels nearly vanish on the night sky (I2 is the check). Accepted: the two icons explain themselves, and the night design ships unchanged rather than dulling the scene for the labels. ## Colors From 2c33055f41bcaec0261d2cc097260396f718da98 Mon Sep 17 00:00:00 2001 From: Lucas Doell Date: Fri, 2 Oct 2026 19:09:50 -0400 Subject: [PATCH 04/20] docs(design): Updates and Daemon Upgrades (ENG-256) Paper Updates page U1-U6 (proposed): Settings -> About with the update row and what each check sends, Restart to update in the app menu, About and the K menu, quiet Upgraded to x.y.z on hosts. Daemon copy moves from update to upgrade per CONTEXT.md; the per-upgrade toast is dropped. --- .agents/skills/product-design/decision-log.md | 1 + .../product-design/references/surfaces.md | 1 + DESIGN.md | 19 +++++++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.agents/skills/product-design/decision-log.md b/.agents/skills/product-design/decision-log.md index 633c3158..325ebbea 100644 --- a/.agents/skills/product-design/decision-log.md +++ b/.agents/skills/product-design/decision-log.md @@ -43,3 +43,4 @@ One line per accepted guidance change: `date | change | evidence`. 2026-10-02 | Setup-only workers nest under their Lead in the sidebar ("setting up · 1m" / "setup failed", never "Dormant"); the command sits in the hover because a 264px row can't hold it with the id and title | Lead decision after C1-G2-setupui; screenshot evidence 2026-10-02 | M3 explorer built from E1: compact folder chains, deleted files kept struck through, the hand on the nearest visible folder, Changes as one flat list (name then folder), three agents then "N more", Delete to the trash without asking (a dialog only where the Host has no trash), and a Worktree or Review Checkout as the explorer's root with "Back to {workspace}". "Agents in" and ages use `text-subtle` where E1 had `text-faint` (rule/faint-is-not-content) | M3-EXPLORER build; spec §2 and §5 2026-10-02 | M3.1 Editor language tooling planned: lazy per-host managed installs with developer-owned prerequisites and Workspace trust; Settings configures composed providers and format-on-save (on, failure saves with error toast); multi-file refactors preview into drafts with guarded resource operations; familiar language assistance and GFM preview via Shift+Cmd+V, Host-relative media and remembered external-image consent | owner answers Q1-Q25 in grill-with-docs; docs/specs/editor-language-tooling-m3.1.md; no implementation or mockups +2026-10-02 | Updates and Daemon Upgrades proposed (Paper Updates page U1–U6): "Restart to update" only in the app menu (plus "Quit and update" on ⌘Q), Settings → About and the K menu, never a toast, badge or dialog; About lists what each update check sends and drops the install ID when checks are off; automatic Daemon Upgrades are quiet (machine bar caption and tooltip, Hosts row), replacing the 2026-10-01 "Daemon upgraded" toast; copy says update for the app and upgrade for daemons | ENG-256 brief; CONTEXT.md Update / Daemon Upgrade; docs/adr/0017 diff --git a/.agents/skills/product-design/references/surfaces.md b/.agents/skills/product-design/references/surfaces.md index 5bba3466..39862384 100644 --- a/.agents/skills/product-design/references/surfaces.md +++ b/.agents/skills/product-design/references/surfaces.md @@ -9,6 +9,7 @@ | Review: PR or agent session turns, risk column, verdicts | `../exemplars/review.md` | Review page, R1 and R2 | | Editor: file tree, git states, inline chat, agent editing a file | `../exemplars/editor.md` | Editor page, E1, E2a, E2, E3 | | Settings: harnesses, usage and plan limits, appearance, hosts | `../exemplars/settings.md` | Settings page, S1–S4 | +| Updates and daemon upgrades: About, restart to update, upgraded hosts | DESIGN.md, Updates and daemon upgrades | Updates page, U1–U6 (proposed) | | Brand, wordmark, app icon, marketing, README art | `../exemplars/brand.md` | Brand and Brand deck pages | A surface not listed has no exemplar yet: check `coverage-gaps.md` and diff --git a/DESIGN.md b/DESIGN.md index 45a90d90..16cec73a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -294,7 +294,7 @@ Dark is the default theme; light is equally supported. Every token exists in bot The Desktop App is Electron (Chromium 152, React). CSS effects are available: shadows, rounded clipping, per-element `backdrop-filter`, springs, custom fonts, and window-level vibrancy. The budgets still rule: display-rate frames (180 Hz measured) and under 1 GB for the whole app. See **Performance** under Motion. -**Assets and source files.** Generated textures live in `design/assets/` (dither frames, state icons, scenes, watercolour washes, halos) and are reproducible from `design/scripts/gen_dither.py` and `design/scripts/gen_textures.py`. Mockups: the Paper file "Polaris — Orchestrator" (https://app.paper.design/file/01M3JV1J857QNW61TGHZYR6GMZ); artboard 5, "Orchestrate · Elevated", is the chosen Orchestrator direction, artboard 7 shows a constellation with a subagent in focus, the Review page holds R1 (pull request, dark) and R2 (agent session turns, light), with M2's proposed states in R3 (pull requests, dark), R4 (review checkout menu, dark), R5 (review checkout states, light), R6 (comment composer, dark), R7 (submit review, dark) and R8 (agent session feedback, light), and the Editor page holds E1 (editor, dark), E2a (selection, dark), E2 (inline chat, dark) and E3 (an agent editing your file, light). The file's Paper tokens mirror this frontmatter as `-dark`/`-light` pairs. +**Assets and source files.** Generated textures live in `design/assets/` (dither frames, state icons, scenes, watercolour washes, halos) and are reproducible from `design/scripts/gen_dither.py` and `design/scripts/gen_textures.py`. Mockups: the Paper file "Polaris — Orchestrator" (https://app.paper.design/file/01M3JV1J857QNW61TGHZYR6GMZ); artboard 5, "Orchestrate · Elevated", is the chosen Orchestrator direction, artboard 7 shows a constellation with a subagent in focus, the Review page holds R1 (pull request, dark) and R2 (agent session turns, light), with M2's proposed states in R3 (pull requests, dark), R4 (review checkout menu, dark), R5 (review checkout states, light), R6 (comment composer, dark), R7 (submit review, dark) and R8 (agent session feedback, light), and the Editor page holds E1 (editor, dark), E2a (selection, dark), E2 (inline chat, dark) and E3 (an agent editing your file, light). The Updates page holds U1–U6, proposed: Settings → About with an update ready (dark) and with automatic checks off (light), the update row's states, the K menu and the app menu with an update ready, a host's "Upgraded to 0.5.0" in the machine bar (light), and Settings → Hosts after Daemon Upgrades. The file's Paper tokens mirror this frontmatter as `-dark`/`-light` pairs. **Mode:** Operate. Scanability, consistency, and native macOS expectations outrank expression. Brand lives in precise details and the pixel world. @@ -591,7 +591,7 @@ The empty-state page for starting an Agent Session. Night-sky scene (dawn in lig ### Settings Settings replaces the three zones inside the one main window (no separate window); ⌘, (Polaris → Settings…), the gear at the right of the sidebar's footer and the K menu ("Settings", "Settings: Appearance"…) open it, and esc returns to where you were. Mockups: Settings page, S1 Harnesses (dark), S2 Usage (dark), S3 Appearance (light), S4 Hosts (dark), S5 GitHub accounts (dark, proposed), S6 Add GitHub account (light, proposed), S7 Reviewer (dark, proposed). -- **Layout.** A 264px `surface-sunken` nav ("Back to orchestrate" with an esc keycap, the `title` "Settings", sections General (Appearance, Sessions) / Agents (Harnesses, Usage) / Machines (Hosts, Attachments), About Polaris with the flat mark and version at the foot) and one centred 680px column on `bg`. The page title is `title`, never `display`; one `body` line in `text-subtle` says what the page controls. +- **Layout.** A 264px `surface-sunken` nav ("Back to orchestrate" with an esc keycap, the `title` "Settings", sections General (Appearance, Sessions) / Agents (Harnesses, Usage) / Machines (Hosts, Attachments), About Polaris with the flat mark and version at the foot, which opens About; with an update ready its caption reads "0.4.0 · update ready") and one centred 680px column on `bg`. The page title is `title`, never `display`; one `body` line in `text-subtle` says what the page controls. - **Groups, not cards.** Settings sit in hairline-bordered groups at the `card` radius with hairline row dividers; a sunken footer strip holds secondary defaults. Rows keep fixed lanes (name, version or value, state, trailing action) so columns align across rows. - **Sessions.** Under General, after Appearance: how new Agent Sessions start and what happens around them, all on this Mac. Four groups. New sessions: "Start on a new worktree" (off; they work in the workspace directory, and each can still choose on New session) and "Branch prefix", a mono field ("polaris/"; empty allowed) whose caption shows a resulting branch; a prefix git wouldn't take gets the failed hairline and isn't saved. While sessions run: "Open output on a turn's first edit" (on) and "Notify when a session needs you" (on; the star and badge still count). Working verbs: the list the Working strip rotates through, as removable chips (a list of one can't lose its last), an add field (↵ adds; blanks and duplicates are ignored, at most 50), and a sunken footer saying Claude Code sessions use `spinnerVerbs` from Claude Code's settings first, with "Reset to the built-in verbs" once the list was edited. Archive: "Delete merged branches on archive" (off; unmerged branches are never deleted). A last `caption` line points to Harnesses for Model, effort and permissions, which stay per Harness in its footer strip. - **Harnesses (S1).** One group per Harness: a 40px Harness tile with its still dither, name, one caption line on how Polaris drives it, and "Ready on N of M hosts". One row per host: version (mono), availability (ready with its sign-in kind, needs sign-in, needs a newer version, not installed) in neutral glyphs and text, never a signal colour, and at most one action ("Sign in in terminal", or "Open setup guide" for a Harness that is missing or too old). A version at or above the Harness's minimum but below the one its driver was tested with is ready, with a quiet `caption` after the status in `text-subtle`: "older than tested (2.1.283)". It never takes a signal colour and never adds an action (no update button); the Harness picker, Settings → Hosts and the first-run footer ("Claude Code 2.1.272, older than tested (2.1.283)") say it the same way. Polaris never installs or updates a Harness: the setup guide is the catalogue's docs link. Sign-in always opens the Harness's own terminal; Polaris never shows a key field (ADR 0001). The footer strip sets what new sessions start with: model, effort, permissions. A Harness that is ready everywhere collapses to its header. @@ -600,14 +600,25 @@ Settings replaces the three zones inside the one main window (no separate window - **Editor.** Under General, after Sessions: "Vim mode" (off) with a caption naming the commands and that Polaris shortcuts keep working in every mode, and "Autosave after a short pause" (off), whose caption says unsaved edits stay on this Mac either way and a save never overwrites a newer version on disk. - **Editor · language integrations (M3.1, planned).** Settings owns language/provider/formatter choices and file associations, with app defaults, per-language and per-workspace overrides, and per-host executable/interpreter/SDK selection. Show each host's installed version, prerequisites and one action (Install, Update, Retry or Restart). Install on first encounter with a toast naming the language and host; updates are explicit. Runtimes and project toolchains remain the user's responsibility. "Format on save" starts on; the formatter respects project configuration. Formatting failure still saves and shows an error toast. Workspaces have a trust choice before tooling executes project code; worktrees inherit it, review checkouts ask separately. Full scope: docs/specs/editor-language-tooling-m3.1.md. - **Hosts (S4).** A table of hosts (name, ssh alias in mono, workspace count, daemon version, Connection State, overflow menu). Connected is a filled `text-subtle` dot with latency; Reconnecting dims the row to ~55% with a hollow dot and elapsed time; Offline is a dashed dot with "last seen"; Needs Attention expands inline under its row with what happened, the evidence in a sunken well, and at most one fix. A changed host key only offers to copy the command. "Add a host" is the page's one primary button. A row opened because something needs the user shows only that (the approval card or the one reason card) and stays open afterwards to say what the install or upgrade did; its settings (name, agent forwarding, off by default, and the remote command) show only when the user opens the row. "Add a host" opens inline under the table: `~/.ssh/config` aliases as a list (wildcards left out), a name, an optional mark and the forwarding switch. Install progress is Polaris's own work, so it is the Starlight dither and the step in words ("Copying the build over SSH and checking its SHA-256"), never a guessed byte bar. The local host row carries "Use this Mac as a host" instead of remove. -- **Hosts · daemon updates (no mockup).** This Mac is the table's first row; each row's caption adds the platform ("ssh studio · Linux arm64"), and the Daemon lane shows the installed version. Under the row's main line, indented to the name, one `caption` line says where its daemon update is: "Update available · 0.4.1 → 0.5.0" with one secondary small "Update" (disabled with "Updates once is connected" while it isn't); the work as the still Starlight dither with the step in words ("Checking", "Copying polaris 0.5.0 · 7.3 of 18.6 MB", "Switching to 0.5.0; agent sessions keep going"), plus a 120px 2px bar in `text-subtle` only while real streamed bytes are counted (never a guessed bar); "Updated to 0.5.0 · 3m ago" with the pixel check for a day; a host with its own choice says so ("Updates its daemon only when you ask"). A failed update is a reason card in the row, like Needs Attention: what happened, that the old daemon still runs, the evidence or command in a sunken well, Retry and at most one more (Copy command, Open in terminal); a connection's own card wins over it. "Keep daemons up to date" is a switch in the table's sunken foot with its caption (upgrades on connect, keeps working agent sessions, never installs without approval); each host overrides it from its row menu's "Daemon updates" radio group (Use the app setting (on), Always keep up to date, Only when I ask) or a select in its opened settings. Notices about an old daemon elsewhere (the `/` menu, the risk column) link here with "Update daemon". +- **Hosts · daemon upgrades (Paper U6, proposed).** A Daemon Upgrade replaces a host's daemon with the build this Mac's Polaris carries, so the words are always "upgrade", never "update" (that is the app's; CONTEXT.md). This Mac is the table's first row; each row's caption adds the platform ("ssh studio · Linux arm64"), and the Daemon lane shows the installed version. Under the row's main line, indented to the name, one `caption` line says where its upgrade is: "0.5.0 available · upgrades its daemon only when you ask" with one secondary small "Upgrade" (disabled with "Upgrades once is connected" while it isn't); "Upgrades to 0.5.0 when it connects" for a host that is offline or reconnecting; the work as the still Starlight dither with the step in words ("Checking", "Copying polaris 0.5.0 · 7.3 of 18.6 MB", "Switching to 0.5.0; agent sessions keep going"), plus a 120px 2px bar in `text-subtle` only while real streamed bytes are counted (never a guessed bar); "Upgraded to 0.5.0 · 3m ago" with the pixel check for a day. A failed upgrade is a reason card in the row, like Needs Attention: what happened, that the old daemon still runs, the evidence or command in a sunken well, Retry and at most one more (Copy command, Open in terminal); a connection's own card wins over it. "Keep daemons up to date" is a switch in the table's sunken foot ("Upgrades each host to this Mac's release when it connects; agent sessions keep working."; a first install still asks for approval); each host overrides it from its row menu's "Daemon upgrades" radio group (Use the app setting (on), Always keep up to date, Only when I ask) or a select in its opened settings. Notices about an old daemon elsewhere (the `/` menu, the risk column) link here with "Upgrade daemon". - **Attachments.** Under Machines, after Hosts. One section per host (each keeps its own settings, ENG-180): the host's name as the heading, then a group. The first row is "Delete attachments", the host's default: when the session is archived (the default), after 1, 7, 30 or 90 days, or never, until cleared. Then one row per workspace on that host, its name and what it has staged ("94 B in 2 files", "Nothing staged"), with a select whose first choice is "Default · …". The sunken footer strip says what the host holds and offers "Clear now" (danger), which asks once more in place ("Cancel", "Delete 2 files") before deleting. A host that isn't connected, or whose daemon predates it, gets one caption line instead of the group. +- **About (Paper U1, U2, U3, proposed).** The nav foot's "About Polaris" opens it, as do Polaris → About Polaris and the K menu ("Settings: About"). No page title: a 64px app icon beside "Polaris 0.4.0" in `title` and one `body` line in `text-subtle` ("Released Sep 18, 2026 · Apple silicon · Apache-2.0"). Then an "Updates" group: the update row (its states below), "Check for updates automatically" (on; "On launch and every 6 hours; downloads in the background", or when off "Off · Polaris checks only when you choose Check now"), "Last checked" with "Today at 14:02 · found 0.5.0" and a secondary "Check now" (the update row carries Check now itself when it has no other action), and a sunken footer, "What Polaris sends with each check": Install ID (mono, shortened, "random, made on this Mac"), Version, Arch, macOS, each a fixed 96px label lane, then "Nothing else: no account, host, workspace or IP. Turning automatic checks off stops both." With checks off the footer reads "What Polaris sends when you check" and the Install ID row says "Not sent while automatic checks are off" (docs/adr/0017). Last, "Daemons on your hosts": one `body` line ("After the update, each host's daemon is upgraded to 0.5.0 when it connects.") and a ghost "Open hosts". +- **The update row (U3).** The group's first row: a 16px glyph, a `label` title, a `caption` line and at most one action besides "Release notes" (ghost). Checking: the Starlight dither, "Checking for updates…", "Asking polaris.lux.dev for the latest release", no action. Up to date: a check in `text-subtle`, "Polaris is up to date", "0.4.0 is the latest release · checked 2h ago", Check now. Downloading: the Starlight dither, "Downloading Polaris 0.5.0", "In the background · nothing to do yet", and no bar or byte count (macOS's updater reports none). Ready: the flat Starlight mark, "Polaris 0.5.0 is ready", "Installs when you restart or quit; agent sessions keep working", Release notes and the page's one primary, "Restart to update". Can't install here (running from Downloads or a disk image): a folder glyph, "Polaris 0.5.0 can't install here", "Move Polaris to Applications, then check again", Show in Finder. Couldn't check: a neutral outlined "!", "Couldn't check for updates", "polaris.lux.dev didn't answer at 14:02 · tries again in 6 hours", Try again; never a signal colour (it is not a Session State or a Severity). Checks off: a dashed circle, "Automatic checks are off", "Last checked Sep 20 · 0.4.0 was the latest then", Check now. - **Setting rows.** A setting's name is 13/400 `text-default` (Paper S7), its caption 12 `text-subtle`; rows, override rows and footer strips pad by the density gap, so every Settings page follows Calm, Balanced and Compact. - **Reviewer (S7).** First in the Review section. A group headed by the Starlight reviewer tile ("Reviews with Codex · GPT-6.1-Sol · High", "For every change, whichever harness made it · read-only"; in automatic mode the caption names the fallback chain and wraps rather than truncating; "Ready on N of M hosts"), then Harness, Model and Effort selects (efforts capitalised), then one row per host in S1's lanes and neutral glyphs, with its review checkout count in `text-subtle`. A host where the reviewer can't run says what its risk summaries do instead ("run rules only") with S1's single action. The reviewer settings live on each host; a change here is written to every connected host. Workspace overrides follow S5's pattern: a sunken strip counting them ("1 workspace overrides the reviewer", one example) with "Edit overrides" opening the list inline beneath it (rule: overrides are edited in place, never in a separate workspace settings page). "When it runs": switches for pull requests on open and agent sessions on open or accept, and "Ask first for large changes" (default over 2,000 changed lines, or Never: rules run, the reviewer waits for "Run reviewer"). "What it checks against": Secrets (always on; critical unless a low-confidence generic match, which is medium), Dangerous patterns (the built-in pack, always on), Review instructions (`.polaris/review.md` and the private host file), and a caption that the reviewer's tokens count in Usage with "Open usage". Later (M6, with Risk memory learning): Risk memory counts with Show all, the proposals-waiting row, a switch for dangerous patterns, and the 7-day reviews-and-tokens figure. - **GitHub accounts (S5, S6).** Under a Review section (Reviewer, GitHub accounts). Accounts as a group, rows on the session-row token: avatar on the harness-tile token, login, "Default" badge on the first (routing order; "Make default" moves an account first), what it covers, "signed in 3 weeks ago" in `text-subtle`; a revoked token reads "Signed out · GitHub refused its token 2 days ago" with "Sign in again", neutral. Then "Account per owner": owner, its workspaces, and an account select; an unmapped owner shows a dashed "Choose account" in `text-default` caption type; an owner whose org blocks Polaris says so under its row with one action, "Get access", whose menu holds Request access, Sign in with SSO and Check again; "Everyone else" uses the default. A sunken strip counts workspace overrides with one example, and "Edit overrides" edits them inline beneath it. "Add account" opens inline (S6): the device code large in mono, Copy code and "Open github.com/login/device" (28px buttons), "Waiting for GitHub" with the expiry in tabular figures, then a caption that GitHub asks for `repo` (full read and write to every repository the account reaches; GitHub has no read-only scope for private repositories) and `read:org`, and one on adding a second account while signed in to another on github.com. The card's padding follows density. - **GitHub Enterprise (no mockup yet).** Below the owners, collapsed to one ghost "Add a GitHub Enterprise server" until there is one. Then a "GitHub Enterprise" group: each server's domain, its account count and OAuth App client ID in a caption, "Add account" (the same inline device flow, its copy naming the server), and a menu with "Manage Polaris on " and removal (the server and its accounts, from this Mac). Adding is inline: Server (domain, URL or API URL) and Client ID, with a caption on who provides the client ID. Copy says "server", never "host": a Host runs a Daemon (CONTEXT.md). Accounts on a server lead their caption with its domain. - **Harness hue in Settings.** Beyond tiles and dither, a Harness hue may fill plan-limit meter cells, chart series and 8px legend squares, since those are identity. It still never colours text, buttons or selection. +### Updates and daemon upgrades + +An Update is the Desktop App replacing itself with a newer Release; a Daemon Upgrade is a host's daemon replaced by the build the app carries (CONTEXT.md). Copy keeps the two words apart, lowercase: "update" for the app, "upgrade" for daemons. Mockups: Updates page, U1–U6 (proposed). + +- **Never interrupts.** An update downloads in the background and waits. There is no toast, badge, dot, dialog or Dock bounce, and nothing appears over a running Turn: the user finds it where they already look for the app's own state, and acts when they choose. +- **"Restart to update" in three places, all at once.** The app menu, under About Polaris: "Restart to update to 0.5.0" (the same slot reads "Check for updates…" otherwise), and Quit reads "Quit and update" (⌘Q installs on the way out; closing the window still only hides Polaris). Settings → About: the ready row, with the view's one primary button, and the nav foot's caption "0.4.0 · update ready". The K menu: an action "Restart to update" ("Polaris 0.5.0 · agent sessions keep working"), found by "update", "restart" or "upgrade", and listed last under Actions on an empty field; "Check for updates" and "Settings: About" sit beside it. The menu bar star's menu stays as it is. +- **A restart keeps sessions.** Agent sessions run in their daemons, so a restart never stops a Turn; the copy says "agent sessions keep working", never asks to confirm, and never counts down. +- **Quiet daemon upgrades (U5, U6).** After an automatic Daemon Upgrade the host says so where hosts already speak: the machine bar's caption under the host's name becomes "Upgraded to 0.5.0" (in place of its workspace count) for an hour or until the user opens that host, and its tooltip reads "Daemon upgraded to 0.5.0 · 3m ago" over "From 0.4.0, to match this Mac. Agent sessions kept working."; Settings → Hosts keeps "Upgraded to 0.5.0 · 3m ago" with the pixel check for a day. No toast, for automatic or user-started upgrades: one release upgrades every host, and a toast per host would be the noise this avoids. In the workspace bar (dark Orchestrator, no mockup) the host's label carries the same tooltip. + ### Onboarding One brand moment, then the real shell; never a wizard. First launch shows a full-bleed scene (night or dawn) with the horizontal lockup, the tagline, one line saying what Polaris drives, and a single "Get started" button in the brand-moment size (see Buttons). A footer states what was found on this Mac (Harness versions, hosts in `~/.ssh/config`). After that, the app opens to the empty Orchestrator, and returns to it whenever no host has a workspace. The stage holds a clearing, the headline "Where does your code live?", and a `surface-raised` setup card with three rows: add a workspace (primary; "Choose folder ⌘O" opens the Open folder dialog on that host, this Mac or remote), connect a host (optional), and start a session. Starting a session never waits for a workspace: with none on the host, the row, New session and ⌘N register the host's home directory as the workspace "home" and open New session in it, as a local shell would. The row is locked only while the host isn't connected. Adding a host uses the same inline components on day 1 and day 60. The first install shows platform, version, SHA-256 and install path in a sunken well, with "Approve and install" and "Not now". Each needs-attention reason (`host-key-unknown`, `host-key-changed`, `auth-failed`, `daemon-not-running`, `protocol-mismatch`) gets an inline card: what happened, the command or stderr line in a well, and at most one fix. A changed host key never gets a one-click fix. Mockups: Onboarding page, O1–O4. @@ -645,7 +656,7 @@ Pasted and dropped files stage on the session's Host straight away and show as s The composer is a plain-text editor (Lexical) where the Harness's **Skills** and **Slash Commands** (CONTEXT.md) become chips (rule/commands-are-chips). -- **The list.** Typing `/` at the start of the message (or `$` anywhere, for a Codex Skill) opens a floating list just above the composer, left-aligned with it, up to 560px wide and 320px tall: `surface-raised`, `card` radius, hairline and the float shadow, fading in over 160ms. Rows are two lines: a 16px icon (Skills: a file with a sparkle; commands: a bolt) in `text-subtle`, the name with its sigil in `label` medium, a faint argument hint (``) and, trailing in `micro`, where it comes from (`project`, `user`, the plugin's name; nothing for built-in ones); then its description in one truncated `caption` line. Skills come first, then commands, each under a `caption` header when both are shown. Typing filters it: prefixes of the name, then of a part (`codex:re` → `codex:review`), then from two letters on anywhere inside; within each of those the user's own **frecency** (how often and how lately they picked it, Sightline's Find scoring) breaks ties, so match quality always wins over habit. A bare `/` leads with up to five frecent picks under a "Recent" header, then Skills and commands as usual. Frecency is kept on this device only, per Host, Workspace and Harness, bounded (120 picks per table, 40 tables). It shows at most 50 rows with "N more · keep typing to narrow" under them. While the Host is still reading the list: one `caption` line, "Reading skills and commands…". Nothing matching closes it; a Harness that lists nothing never opens it. **A Host whose Daemon is too old to list them** never shows an empty list: in the list's place, one `caption` line, "Skills need a newer daemon on Mac Studio", with a secondary "Upgrade daemon ↵" that runs that Host's upgrade (as Settings → Hosts does). It follows the upgrade: "Upgrading the daemon on Mac Studio…", "Couldn't upgrade the daemon on Mac Studio: …" with "Try again", then the list once the Host reconnects. Any Host's upgrade says so in a Polaris toast, "Daemon upgraded · This Mac · 0.0.0-dev.399 → 0.0.0-dev.412". +- **The list.** Typing `/` at the start of the message (or `$` anywhere, for a Codex Skill) opens a floating list just above the composer, left-aligned with it, up to 560px wide and 320px tall: `surface-raised`, `card` radius, hairline and the float shadow, fading in over 160ms. Rows are two lines: a 16px icon (Skills: a file with a sparkle; commands: a bolt) in `text-subtle`, the name with its sigil in `label` medium, a faint argument hint (``) and, trailing in `micro`, where it comes from (`project`, `user`, the plugin's name; nothing for built-in ones); then its description in one truncated `caption` line. Skills come first, then commands, each under a `caption` header when both are shown. Typing filters it: prefixes of the name, then of a part (`codex:re` → `codex:review`), then from two letters on anywhere inside; within each of those the user's own **frecency** (how often and how lately they picked it, Sightline's Find scoring) breaks ties, so match quality always wins over habit. A bare `/` leads with up to five frecent picks under a "Recent" header, then Skills and commands as usual. Frecency is kept on this device only, per Host, Workspace and Harness, bounded (120 picks per table, 40 tables). It shows at most 50 rows with "N more · keep typing to narrow" under them. While the Host is still reading the list: one `caption` line, "Reading skills and commands…". Nothing matching closes it; a Harness that lists nothing never opens it. **A Host whose Daemon is too old to list them** never shows an empty list: in the list's place, one `caption` line, "Skills need a newer daemon on Mac Studio", with a secondary "Upgrade daemon ↵" that runs that Host's upgrade (as Settings → Hosts does). It follows the upgrade: "Upgrading the daemon on Mac Studio…", "Couldn't upgrade the daemon on Mac Studio: …" with "Try again", then the list once the Host reconnects. The upgrade itself is reported quietly, as every Daemon Upgrade is (see Updates and daemon upgrades): no toast. - **Keyboard-first.** The editor keeps focus: ↑ ↓ move the highlight (`fill-selected`), ↵ or ⇥ pick, esc closes the list for that word (a second esc stops a Working Turn as before). A click picks too; hovering moves the highlight. - **The chip.** Picking one replaces the typed word with a chip: its name in `label` medium `text-strong` on `fill-selected`, `control` radius, 1px by 5px padding, with its kind's icon in `text-subtle` standing in for the sigil. It sits inline in the text and never changes the line height. The caret and Backspace take it whole; copying it gives its sigil and name. A listed name typed through with a space after it (`/compact `) becomes a chip too, as does a restored draft's. Chips are neutral: the Harness hue stays in its three places. - **What runs.** Each entry carries how it runs (`harness.commands`): sent as text in the Turn (most Skills and commands; a Codex custom prompt goes expanded), turned by the driver into the Harness's own call (Codex `/compact`, `/review`; OpenCode's commands), or done by Polaris itself, in which case picking it leaves no chip and no text: `/model` and `/effort` open the Model menu, `/clear` and `/new` start a new session, `/diff` shows Output, `/usage` and `/cost` open Settings → Usage. Commands only a Harness's terminal UI can run (themes, `/config`, `/mcp`, `/heapdump`…) are never listed. The per-Harness table is in `apps/daemon/src/harness/README.md` and each driver's README. From 810475548030ecabd5e5c3e349d4addfd3e63793 Mon Sep 17 00:00:00 2001 From: Lucas Doell Date: Fri, 2 Oct 2026 19:11:12 -0400 Subject: [PATCH 05/20] Add release packaging with signed Daemons and notarised app Updates --- apps/desktop/README.md | 23 ++- apps/desktop/scripts/package.ts | 71 ++++++-- apps/desktop/scripts/packageCheck.ts | 52 +++++- apps/desktop/scripts/packaging/command.ts | 10 ++ .../packaging/entitlements/betterleaks.plist | 8 + .../packaging/entitlements/daemon.plist | 12 ++ .../packaging/entitlements/electron.plist | 8 + apps/desktop/scripts/packaging/index.ts | 10 ++ .../scripts/packaging/manifest.test.ts | 158 ++++++++++++++++++ apps/desktop/scripts/packaging/manifest.ts | 133 +++++++++++++++ .../desktop/scripts/packaging/release.test.ts | 132 +++++++++++++++ apps/desktop/scripts/packaging/release.ts | 143 ++++++++++++++++ 12 files changed, 742 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/scripts/packaging/command.ts create mode 100644 apps/desktop/scripts/packaging/entitlements/betterleaks.plist create mode 100644 apps/desktop/scripts/packaging/entitlements/daemon.plist create mode 100644 apps/desktop/scripts/packaging/entitlements/electron.plist create mode 100644 apps/desktop/scripts/packaging/index.ts create mode 100644 apps/desktop/scripts/packaging/manifest.test.ts create mode 100644 apps/desktop/scripts/packaging/manifest.ts create mode 100644 apps/desktop/scripts/packaging/release.test.ts create mode 100644 apps/desktop/scripts/packaging/release.ts diff --git a/apps/desktop/README.md b/apps/desktop/README.md index d538dfca..8d7c0d44 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -25,9 +25,28 @@ node scripts/emptyScreens.ts --out [--build] # the empty states and t **Polaris Dev.app (macOS).** Dev and every script that launches the app (smoke, screens, budgets) run `out/dev/Polaris Dev.app` rather than node_modules' `Electron.app`, so the app menu, Dock, ⌘⇥ and Activity Monitor say "Polaris Dev" (helpers too) and the Dock shows the dusk icon. `scripts/lib/devBundle.ts` makes it on first use: a clonefile copy (`cp -c`) of Electron.app, its executable and helpers renamed, `CFBundleName`/`CFBundleDisplayName` "Polaris Dev", `CFBundleIdentifier` `dev.lux.polaris.dev`, `design/assets/app-icon/dusk/PolarisDev.icns` as the icon, re-signed ad hoc. That takes about 1.5 s. After that `out/dev/stamp.json` (Electron's path and Info.plist, the icon's hash, the patch version) keeps it until one of those changes. `POLARIS_DESKTOP_STOCK_ELECTRON=1` runs stock Electron instead; Linux and Windows always do. userData doesn't move: dev still uses `POLARIS_DESKTOP_USER_DATA`, or `/polaris-desktop-dev-user-data`, and `nameApp` keeps the path it had before renaming. `build` and `package` still start from stock Electron. -**Packaging** (`scripts/package.ts`, `@electron/packager`): builds main and the renderer, builds the Daemon for every platform (`apps/daemon` `build`), stages an app directory holding only `out/{main,preload,renderer}` and a minimal `package.json` (the bundles are self-contained; only `electron` is external), and packages it with appId `dev.lux.polaris`, the developer-tools category, the generated icon (`design/assets/app-icon/Polaris.icns`, from `design/scripts/gen_app_icon.py`), and `extraResource` for `Resources/daemon/` (every Daemon build plus `manifest.json`, which `machines/builds.ts` reads when packaged) and `Resources/icon.png` (the Linux window icon). The app is unsigned: Gatekeeper asks on first open (right-click → Open). The name and icon also apply in dev (`src/main/identity.ts`: `app.setName`, About, and the dusk Dock icon), and on macOS dev runs `Polaris Dev.app` (above), so the Dock and the app menu read "Polaris Dev". +**Packaging** (`scripts/package.ts`, `@electron/packager`): builds main and the renderer, builds the Daemon for every platform (`apps/daemon` `build`), stages an app directory holding only `out/{main,preload,renderer}` and a minimal `package.json` (the bundles are self-contained; only `electron` is external), and packages it with appId `dev.lux.polaris`, the developer-tools category, the generated icon (`design/assets/app-icon/Polaris.icns`, from `design/scripts/gen_app_icon.py`), and `extraResource` for `Resources/daemon/` (every Daemon build plus `manifest.json`, which `machines/builds.ts` reads when packaged) and `Resources/icon.png` (the Linux window icon). The name and icon also apply in dev (`src/main/identity.ts`: `app.setName`, About, and the dusk Dock icon), and on macOS dev runs `Polaris Dev.app` (above), so the Dock and the app menu read "Polaris Dev". -Follow-ups: signing with a Developer ID and notarising (`osxSign`, `osxNotarize`, hardened runtime and entitlements for the JIT), a DMG or zip for download, Linux AppImage/deb (today `--linux` makes an unpacked `Polaris-linux-x64/`), a macOS 26 `.icon` (Icon Composer) beside the `.icns` for the tinted and clear modes, and slimming `Resources/daemon` (about 485 MB for five builds; the packaged app is about 790 MB). +`bun run --cwd apps/desktop package --release` builds every Daemon with `--release`, using the shared `apps/desktop/package.json` and `apps/daemon/package.json` version. A mismatch fails before building; `--reuse-daemon` is refused in release mode so stale dev builds cannot ship. Without `--release`, Daemons retain their commit-qualified dev versions; `--reuse-daemon` remains available for local packaging. The macOS build also produces `out/dist/Polaris--arm64-mac.zip`, with `Polaris.app` at the archive root. `ditto --keepParent --sequesterRsrc` preserves Electron's framework symlinks and the stapled ticket for Updates. + +With no Apple credentials, packaging skips signing and notarisation and produces an unsigned app and zip. For a signed build, first import the Developer ID Application certificate from `MACOS_CERT_P12` using `MACOS_CERT_PASSWORD` into an unlocked keychain (the Release workflow owns import and cleanup). Set these environment variables: + +| Variable | Value | +|---|---| +| `APPLE_TEAM_ID` | Team owning the Developer ID Application identity. | +| `APPLE_API_KEY` | Absolute path to the App Store Connect **Team Key** `.p8` file, not its contents. | +| `APPLE_API_KEY_ID` | API key ID. | +| `APPLE_API_ISSUER` | Team Key issuer ID. | +| `MACOS_SIGNING_IDENTITY` | Optional exact certificate name or SHA-1 identity hash; required if the team has several valid Developer ID Application identities. | +| `MACOS_KEYCHAIN` | Optional keychain path, shared by identity lookup, tool signing and Electron signing. | + +Partial credentials, a missing key file, a missing/ambiguous identity, signing errors or notarisation errors fail the build. Certificate material stays in the keychain; packaging never imports it or logs its password. macOS and Xcode with `notarytool` are required for the signed path. + +Packaging signs `darwin-arm64/polaris` and `betterleaks` with Developer ID, a secure timestamp and hardened runtime **before** copying them into the app. The Bun Daemon uses `packaging/entitlements/daemon.plist`: JIT and unsigned executable memory, plus disabled library validation for the embedded fff/ast-grep native modules. Betterleaks uses `packaging/entitlements/betterleaks.plist` with unsigned executable memory for its go-re2/wazero regex engine. A hardened-runtime ad-hoc check of the pinned binary passes `version` without exceptions but is killed during scanning; unsigned executable memory alone makes the full planted-token selftest pass (JIT or disabled library validation alone does not). The signed Daemon runs `selftest` (including native-module loading), and Betterleaks runs `version`. Only then are their SHA-256s and sizes written to `apps/daemon/dist/manifest.json`. Linux binaries and hashes remain unchanged. The same signed bytes and manifest ship in `Resources/daemon`, which the Client uploads to remote Hosts; `osxSign` explicitly skips this subtree to preserve the signatures and hashes. Packaging checks every manifest file before signing and again after app packaging, so later tool mutation fails the build. + +Packager's `osxSign` signs Electron with hardened runtime, `continueOnError: false` and `packaging/entitlements/electron.plist` (`allow-jit` only). `osxNotarize` submits via the API key, waits and staples the app; packaging verifies its code signature and stapled ticket before creating the final zip. Entitlements follow [Bun's executable signing guide](https://bun.sh/docs/guides/runtime/codesign-macos-executable) and the installed [`@electron/notarize` guidance](https://github.com/electron/notarize#prerequisites); Electron does not receive Bun's broader runtime exceptions. A real Developer ID/notarisation dry run is required once Apple enrollment and secrets are available (ENG-253/ENG-262). + +Follow-ups: a DMG for download (ENG-258), Linux AppImage/deb (today `--linux` makes an unpacked `Polaris-linux-x64/`), a macOS 26 `.icon` (Icon Composer) beside the `.icns` for the tinted and clear modes, and slimming `Resources/daemon` (about 485 MB for five builds; the packaged app is about 790 MB). `dev` connects the local Host to `~/.polaris/daemon.sock` when a Daemon answers there; otherwise it starts a dev Daemon from source with its own home and the scripted bench Harness, and keeps it across restarts (ADR 0007). Builds: Vite for the renderer, `Bun.build` for main and preload (ADR 0008). diff --git a/apps/desktop/scripts/package.ts b/apps/desktop/scripts/package.ts index 4f09f908..8d3e7a07 100644 --- a/apps/desktop/scripts/package.ts +++ b/apps/desktop/scripts/package.ts @@ -3,16 +3,29 @@ * Packages the Desktop App with @electron/packager: `out/dist/Polaris-darwin-arm64/Polaris.app` * (and, with `--linux`, `Polaris-linux-x64/`), appId `dev.lux.polaris`, the generated app * icon, and every Daemon build in `Resources/daemon` (see src/main/machines/README.md). - * Unsigned: signing and notarisation are a follow-up (README, Packaging). + * Signs and notarises when Apple credentials are provided (README, Packaging). * * bun scripts/package.ts this Mac (darwin arm64) * bun scripts/package.ts --linux …and Linux x64 * bun scripts/package.ts --reuse-daemon keep apps/daemon/dist as it is + * bun scripts/package.ts --release matching release versions, fresh Daemons */ import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { packager } from "@electron/packager"; +import { Schema } from "effect"; import { APP_DIR, buildMain, buildRenderer, OUT_DIR, REPO_ROOT } from "./lib/build.ts"; +import { + assertReleaseVersions, + daemonBuildCommand, + macSigningOptions, + resolveSigning, + signDaemonBuilds, + signingCredentials, + updateZip, + verifyDaemonManifest, +} from "./packaging/index.ts"; +import { run } from "./packaging/command.ts"; const args = process.argv.slice(2); @@ -24,11 +37,34 @@ const DAEMON_DIST = join(REPO_ROOT, "apps/daemon/dist"); const STAGE = join(OUT_DIR, "package"); -const run = (cmd: string[], cwd: string) => { - const result = Bun.spawnSync(cmd, { cwd, stdout: "inherit", stderr: "inherit" }); +const release = args.includes("--release"); - if (result.exitCode !== 0) throw new Error(`${cmd.join(" ")} failed`); -}; +const decodePackage = Schema.decodeUnknownSync( + Schema.Struct({ + version: Schema.String, + devDependencies: Schema.Record(Schema.String, Schema.String), + }) +); + +const pkg = decodePackage(await Bun.file(join(APP_DIR, "package.json")).json()); + +if (release) { + const daemon = Schema.decodeUnknownSync(Schema.Struct({ version: Schema.String }))( + await Bun.file(join(REPO_ROOT, "apps/daemon/package.json")).json() + ); + + assertReleaseVersions(pkg.version, daemon.version, args.includes("--reuse-daemon")); +} + +const signing = resolveSigning(signingCredentials(process.env)); + +const ENTITLEMENTS = join(import.meta.dir, "packaging/entitlements"); + +console.log( + signing === null + ? "no Apple credentials: packaging unsigned" + : "signing and notarising with Developer ID" +); if (!existsSync(join(ICONS, "Polaris.icns"))) throw new Error( @@ -40,7 +76,11 @@ await buildRenderer(); await buildMain({ minify: true }); if (!args.includes("--reuse-daemon") || !existsSync(join(DAEMON_DIST, "manifest.json"))) - run(["bun", "run", "build"], join(REPO_ROOT, "apps/daemon")); + console.log(run(daemonBuildCommand(REPO_ROOT, release)).trim()); + +verifyDaemonManifest(DAEMON_DIST, release ? pkg.version : undefined); + +signDaemonBuilds(DAEMON_DIST, signing, ENTITLEMENTS); // The app directory holds only what runs: the bundles are self-contained (just `electron`). rmSync(STAGE, { recursive: true, force: true }); @@ -56,12 +96,6 @@ mkdirSync(resources, { recursive: true }); for (const dir of ["main", "preload", "renderer"]) cpSync(join(OUT_DIR, dir), join(app, "out", dir), { recursive: true }); -// SAFETY: our own package.json, which always has these fields. -const pkg = (await Bun.file(join(APP_DIR, "package.json")).json()) as { - readonly version: string; - readonly devDependencies: Readonly>; -}; - writeFileSync( join(app, "package.json"), JSON.stringify( @@ -111,8 +145,21 @@ for (const target of targets) { asar: true, prune: false, quiet: true, + ...macSigningOptions(target.platform === "darwin" ? signing : null, ENTITLEMENTS), }); + if (path === undefined) throw new Error(`packager returned no ${target.platform} output`); + + if (target.platform === "darwin") { + const bundle = join(path, "Polaris.app"); + + verifyDaemonManifest( + join(bundle, "Contents/Resources/daemon"), + release ? pkg.version : undefined + ); + console.log(`archived ${updateZip(bundle, pkg.version, join(OUT_DIR, "dist"), signing)}`); + } + console.log(`packaged ${path}`); } diff --git a/apps/desktop/scripts/packageCheck.ts b/apps/desktop/scripts/packageCheck.ts index b813de5e..e2971e63 100644 --- a/apps/desktop/scripts/packageCheck.ts +++ b/apps/desktop/scripts/packageCheck.ts @@ -9,9 +9,12 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { _electron as electron } from "playwright-core"; +import type { PolarisApi } from "../src/shared/api.ts"; import { startDaemon } from "./lib/daemon.ts"; import { APP_DIR } from "./lib/electron.ts"; +declare const window: { readonly polaris: PolarisApi }; + const args = process.argv.slice(2); const at = args.indexOf("--screenshot"); @@ -32,10 +35,12 @@ const daemon = await startDaemon({ home, benchHarness: true, userHome: join(home const app = await electron.launch({ executablePath: join(bundle, "Contents/MacOS/Polaris"), + args: ["--use-mock-keychain"], env: { ...process.env, POLARIS_DESKTOP_LOCAL_SOCKET: daemon.socketPath, POLARIS_DESKTOP_USER_DATA: userData, + POLARIS_DESKTOP_BENCH_HARNESS: "1", }, }); @@ -72,10 +77,49 @@ try { check("Daemon builds", facts.daemonBuilds, "Resources/daemon/manifest.json"); check("window icon", facts.icon, "Resources/icon.png"); - await page - .locator('[data-host="local"][data-connection="connected"]') - .first() - .waitFor({ timeout: 30_000 }); + // The empty-workspace home screen does not render the Host bar. + await page.waitForFunction(() => Boolean(window.polaris)); + await page.evaluate( + () => + new Promise((resolve, reject) => { + let lastStatus = "no Host feed items"; + + const timer = setTimeout(() => { + cancel(); + reject(new Error(`local Host did not connect in 30 s: ${lastStatus}`)); + }, 30_000); + + const cancel = window.polaris.subscribe( + "hosts", + {}, + { + items: (items) => { + lastStatus = JSON.stringify( + items.flatMap((hosts) => + hosts.map((host) => ({ key: host.key, status: host.status })) + ) + ); + + if ( + !items.some((hosts) => + hosts.some((host) => host.key === "local" && host.status.state === "connected") + ) + ) + return; + + clearTimeout(timer); + cancel(); + resolve(); + }, + end: () => { + clearTimeout(timer); + cancel(); + reject(new Error("Host feed ended before the local Host connected")); + }, + } + ); + }) + ); check("local Daemon", true, "connected"); if (shot !== null) await page.screenshot({ path: shot }); diff --git a/apps/desktop/scripts/packaging/command.ts b/apps/desktop/scripts/packaging/command.ts new file mode 100644 index 00000000..a00767d5 --- /dev/null +++ b/apps/desktop/scripts/packaging/command.ts @@ -0,0 +1,10 @@ +export type Run = (argv: ReadonlyArray) => string; + +export const run: Run = (argv) => { + const result = Bun.spawnSync([...argv], { stdout: "pipe", stderr: "pipe" }); + + if (result.exitCode !== 0) + throw new Error(`${argv[0]} failed (${result.exitCode})\n${result.stderr.toString()}`); + + return result.stdout.toString(); +}; diff --git a/apps/desktop/scripts/packaging/entitlements/betterleaks.plist b/apps/desktop/scripts/packaging/entitlements/betterleaks.plist new file mode 100644 index 00000000..d6b93bc0 --- /dev/null +++ b/apps/desktop/scripts/packaging/entitlements/betterleaks.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/apps/desktop/scripts/packaging/entitlements/daemon.plist b/apps/desktop/scripts/packaging/entitlements/daemon.plist new file mode 100644 index 00000000..9a279dc8 --- /dev/null +++ b/apps/desktop/scripts/packaging/entitlements/daemon.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/apps/desktop/scripts/packaging/entitlements/electron.plist b/apps/desktop/scripts/packaging/entitlements/electron.plist new file mode 100644 index 00000000..446fe171 --- /dev/null +++ b/apps/desktop/scripts/packaging/entitlements/electron.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-jit + + + diff --git a/apps/desktop/scripts/packaging/index.ts b/apps/desktop/scripts/packaging/index.ts new file mode 100644 index 00000000..1332fcd1 --- /dev/null +++ b/apps/desktop/scripts/packaging/index.ts @@ -0,0 +1,10 @@ +export { signDaemonBuilds, verifyDaemonManifest } from "./manifest.ts"; + +export { + assertReleaseVersions, + daemonBuildCommand, + macSigningOptions, + resolveSigning, + signingCredentials, + updateZip, +} from "./release.ts"; diff --git a/apps/desktop/scripts/packaging/manifest.test.ts b/apps/desktop/scripts/packaging/manifest.test.ts new file mode 100644 index 00000000..40a81560 --- /dev/null +++ b/apps/desktop/scripts/packaging/manifest.test.ts @@ -0,0 +1,158 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { createHash } from "node:crypto"; +import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { PLATFORMS, loadBuilds } from "@polaris/client/install"; +import { signDaemonBuilds, verifyDaemonManifest } from "./manifest.ts"; +import { signingCredentials, type Signing } from "./release.ts"; + +const dirs: string[] = []; + +afterEach(() => { + for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); +}); + +const hash = (bytes: string | Buffer) => createHash("sha256").update(bytes).digest("hex"); + +const fixture = () => { + const dist = mkdtempSync(join(tmpdir(), "polaris-release-test-")); + dirs.push(dist); + + const platforms = Object.fromEntries( + PLATFORMS.map((platform) => { + mkdirSync(join(dist, platform)); + + const files = Object.fromEntries( + ["polaris", "betterleaks"].map((name) => { + const bytes = `${platform}/${name}`; + writeFileSync(join(dist, platform, name), bytes); + + return [ + name, + { + sha256: hash(bytes), + size: Buffer.byteLength(bytes), + executable: name === "betterleaks", + }, + ]; + }) + ); + + return [ + platform, + { target: `bun-${platform}`, binary: "polaris", sha256: files.polaris!.sha256, files }, + ]; + }) + ); + + writeFileSync( + join(dist, "manifest.json"), + JSON.stringify({ + version: "0.1.0-rc.1", + commit: "commit", + fff: { package: "fff", version: "1", embedded: true }, + astGrep: { package: "ast-grep", version: "1", embedded: true }, + betterleaks: { version: "1", file: "betterleaks" }, + platforms, + }) + ); + + return dist; +}; + +const signing: Signing = { + ...signingCredentials({ + APPLE_TEAM_ID: "TEAM123456", + APPLE_API_KEY: "/fake/key.p8", + APPLE_API_KEY_ID: "KEY1234567", + APPLE_API_ISSUER: "issuer", + })!, + identity: "test-identity", + keychain: "/fake/keychain", +}; + +describe("shipped Daemon manifest", () => { + test("hashes signed bytes, keeps Linux builds, and survives staging for remote Host installs", () => { + const dist = fixture(); + const linuxBefore = readFileSync(join(dist, "linux-x64/polaris")); + const commands: ReadonlyArray[] = []; + signDaemonBuilds(dist, signing, "/entitlements", (argv) => { + commands.push(argv); + + if (argv[0] === "codesign" && argv.includes("--sign")) { + const binary = argv.at(-1)!; + writeFileSync(binary, Buffer.concat([readFileSync(binary), Buffer.from("-signed")])); + } + + return argv[1] === "selftest" ? "polaris 0.1.0-rc.1 darwin-arm64\nfff ok" : ""; + }); + verifyDaemonManifest(dist, "0.1.0-rc.1"); + expect(readFileSync(join(dist, "linux-x64/polaris"))).toEqual(linuxBefore); + const signed = commands.filter((argv) => argv.includes("--sign")); + expect(signed).toHaveLength(2); + expect(signed[0]).toContain("runtime"); + expect(signed[0]).toContain("--timestamp"); + expect(signed[0]).toContain("/fake/keychain"); + expect(signed[0]).toContain("/entitlements/daemon.plist"); + expect(signed[1]).toContain("/entitlements/betterleaks.plist"); + const stage = join(dist, "staged"); + mkdirSync(stage); + + for (const name of [...PLATFORMS, "manifest.json"]) + cpSync(join(dist, name), join(stage, name), { recursive: true }); + verifyDaemonManifest(stage, "0.1.0-rc.1"); + const mac = loadBuilds(stage).find((build) => build.platform === "darwin-arm64")!; + + for (const file of mac.files) { + expect(file.sha256).toBe(hash(readFileSync(file.path))); + expect(readFileSync(file.path).toString()).toEndWith("-signed"); + } + + expect(mac.sha256).toBe(mac.files[0]!.sha256); + expect(mac.files[1]!.executable).toBe(true); + }); + + test("unsigned builds keep their manifest and never call signing tools", () => { + const dist = fixture(); + const before = readFileSync(join(dist, "manifest.json")); + signDaemonBuilds(dist, null, "/entitlements", () => { + throw new Error("unexpected signing"); + }); + expect(readFileSync(join(dist, "manifest.json"))).toEqual(before); + }); + + test("rejects mismatched, stale or incomplete release builds", () => { + const dist = fixture(); + expect(() => verifyDaemonManifest(dist, "0.2.0")).toThrow("does not match release"); + writeFileSync(join(dist, "linux-x64/polaris"), "changed"); + expect(() => verifyDaemonManifest(dist)).toThrow("bytes differ"); + const incomplete = fixture(); + const builds = loadBuilds(incomplete); + writeFileSync( + join(incomplete, "manifest.json"), + readFileSync(join(incomplete, "manifest.json"), "utf8").replace( + '"linux-arm64-musl":', + '"unexpected":' + ) + ); + expect(builds).toHaveLength(5); + expect(() => verifyDaemonManifest(incomplete, "0.1.0-rc.1")).toThrow( + "missing linux-arm64-musl" + ); + }); + + test("a failed signer or signed selftest cannot produce a refreshed manifest", () => { + const dist = fixture(); + const before = readFileSync(join(dist, "manifest.json")); + expect(() => + signDaemonBuilds(dist, signing, "/entitlements", () => { + throw new Error("signing failed"); + }) + ).toThrow("signing failed"); + expect(() => signDaemonBuilds(dist, signing, "/entitlements", () => "wrong version")).toThrow( + "selftest" + ); + expect(readFileSync(join(dist, "manifest.json"))).toEqual(before); + }); +}); diff --git a/apps/desktop/scripts/packaging/manifest.ts b/apps/desktop/scripts/packaging/manifest.ts new file mode 100644 index 00000000..1bc52e27 --- /dev/null +++ b/apps/desktop/scripts/packaging/manifest.ts @@ -0,0 +1,133 @@ +import { createHash } from "node:crypto"; +import { readFileSync, statSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { PLATFORMS } from "@polaris/client/install"; +import { Schema } from "effect"; +import { type Run, run } from "./command.ts"; +import type { Signing } from "./release.ts"; + +const FileEntry = Schema.Struct({ + sha256: Schema.String, + size: Schema.Number, + executable: Schema.optional(Schema.Boolean), +}); + +const Embedded = Schema.Struct({ + package: Schema.String, + version: Schema.String, + embedded: Schema.Boolean, +}); + +const Manifest = Schema.Struct({ + version: Schema.String, + commit: Schema.String, + fff: Embedded, + astGrep: Embedded, + betterleaks: Schema.Struct({ version: Schema.String, file: Schema.String }), + platforms: Schema.Record( + Schema.String, + Schema.Struct({ + target: Schema.String, + binary: Schema.String, + sha256: Schema.String, + files: Schema.Record(Schema.String, FileEntry), + }) + ), +}); + +const decodeManifest = Schema.decodeUnknownSync(Schema.fromJsonString(Manifest)); + +const readManifest = (dist: string) => + decodeManifest(readFileSync(join(dist, "manifest.json"), "utf8")); + +const fileFacts = (path: string) => ({ + sha256: createHash("sha256").update(readFileSync(path)).digest("hex"), + size: statSync(path).size, +}); + +const verifyRelease = (manifest: typeof Manifest.Type, version: string): void => { + if (manifest.version !== version) + throw new Error( + `Daemon manifest version ${manifest.version} does not match release ${version}` + ); + + for (const platform of PLATFORMS) { + if (!manifest.platforms[platform]) throw new Error(`release Daemon missing ${platform}`); + } +}; + +export const verifyDaemonManifest = (dist: string, version?: string): void => { + const manifest = readManifest(dist); + + if (version !== undefined) verifyRelease(manifest, version); + + for (const [platform, build] of Object.entries(manifest.platforms)) { + if (build.sha256 !== build.files[build.binary]?.sha256) + throw new Error(`${platform}: binary SHA differs from files entry`); + + for (const [name, entry] of Object.entries(build.files)) { + const facts = fileFacts(join(dist, platform, name)); + + if (facts.sha256 !== entry.sha256 || facts.size !== entry.size) + throw new Error(`${platform}/${name}: bytes differ from Daemon manifest`); + } + } +}; + +export const signDaemonBuilds = ( + dist: string, + signing: Signing | null, + entitlementsDir: string, + execute: Run = run +): void => { + verifyDaemonManifest(dist); + + if (signing === null) return; + + const manifest = readManifest(dist); + const build = manifest.platforms["darwin-arm64"]; + + if (!build || build.binary !== "polaris" || !build.files.polaris || !build.files.betterleaks) + throw new Error("missing darwin-arm64 polaris or betterleaks"); + + const files = { ...build.files }; + + for (const name of ["polaris", "betterleaks"]) { + const binary = join(dist, "darwin-arm64", name); + + execute([ + "codesign", + "--force", + "--sign", + signing.identity, + "--options", + "runtime", + "--timestamp", + ...(signing.keychain ? ["--keychain", signing.keychain] : []), + "--entitlements", + join(entitlementsDir, name === "polaris" ? "daemon.plist" : "betterleaks.plist"), + binary, + ]); + execute(["codesign", "--verify", "--strict", binary]); + files[name] = { ...files[name]!, ...fileFacts(binary) }; + } + + const binary = join(dist, "darwin-arm64", "polaris"); + const output = execute([binary, "selftest"]).trim(); + + if (output.split("\n")[0] !== `polaris ${manifest.version} darwin-arm64`) + throw new Error("signed Daemon selftest did not report the expected version and platform"); + + execute([join(dist, "darwin-arm64", "betterleaks"), "version"]); + + const signedManifest = { + ...manifest, + platforms: { + ...manifest.platforms, + "darwin-arm64": { ...build, sha256: files.polaris!.sha256, files }, + }, + }; + + writeFileSync(join(dist, "manifest.json"), `${JSON.stringify(signedManifest, null, 2)}\n`); + verifyDaemonManifest(dist); +}; diff --git a/apps/desktop/scripts/packaging/release.test.ts b/apps/desktop/scripts/packaging/release.test.ts new file mode 100644 index 00000000..bb7a3616 --- /dev/null +++ b/apps/desktop/scripts/packaging/release.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, test } from "bun:test"; +import { join } from "node:path"; +import { + assertReleaseVersions, + daemonBuildCommand, + macSigningOptions, + preserveDaemonSignature, + selectIdentity, + signingCredentials, + type Signing, + updateZip, +} from "./release.ts"; + +const env = { + APPLE_TEAM_ID: "TEAM123456", + APPLE_API_KEY: "/tmp/key.p8", + APPLE_API_KEY_ID: "KEY1234567", + APPLE_API_ISSUER: "test-issuer", +}; + +const identity = "Developer ID Application: Polaris (TEAM123456)"; + +const hash = "A".repeat(40); + +const signing: Signing = { ...signingCredentials(env)!, identity: hash }; + +describe("release packaging", () => { + test("release validates versions and rebuilds all Daemons with --release", () => { + expect(() => assertReleaseVersions("0.1.0-rc.1", "0.1.0-rc.1", false)).not.toThrow(); + expect(() => assertReleaseVersions("0.1.0", "0.0.0", false)).toThrow("version mismatch"); + expect(() => assertReleaseVersions("0.1.0", "0.1.0", true)).toThrow("--reuse-daemon"); + expect(daemonBuildCommand("/repo", true)).toEqual([ + process.execPath, + "/repo/scripts/build-daemon.ts", + "--release", + ]); + expect(daemonBuildCommand("/repo", false)).not.toContain("--release"); + }); + + test("no credentials skip signing; incomplete credentials fail closed", () => { + expect(signingCredentials({ APPLE_TEAM_ID: "" })).toBeNull(); + expect(macSigningOptions(null, "/entitlements")).toEqual({}); + + for (const key of Object.keys(env)) { + expect(() => signingCredentials({ ...env, [key]: undefined })).toThrow( + "incomplete signing credentials" + ); + } + + expect(() => signingCredentials({ MACOS_CERT_P12: "present" })).toThrow( + "incomplete signing credentials" + ); + }); + + test("identity selection requires Developer ID and the configured team", () => { + const output = `1) ${hash} "${identity}"\n2) ${"B".repeat(40)} "Apple Development: Test (TEAM123456)"`; + expect(selectIdentity(signingCredentials(env)!, output)).toBe(hash); + expect(() => + selectIdentity(signingCredentials(env)!, output.replace("TEAM123456", "OTHERTEAM1")) + ).toThrow("expected one valid"); + expect(() => + selectIdentity(signingCredentials(env)!, `${output}\n3) ${"C".repeat(40)} "${identity}"`) + ).toThrow("expected one valid"); + expect( + selectIdentity( + { ...signingCredentials(env)!, identity: hash }, + `${output}\n3) ${"C".repeat(40)} "${identity}"` + ) + ).toBe(hash); + expect(() => selectIdentity({ ...signingCredentials(env)!, identity: "-" }, output)).toThrow( + "expected one valid" + ); + }); + + test("Electron signing fails on errors and preserves shipped Daemon signatures", () => { + const options = macSigningOptions(signing, "/entitlements"); + const osxSign = options.osxSign; + + if (!osxSign || osxSign === true) throw new Error("missing explicit signing options"); + expect(osxSign.continueOnError).toBe(false); + expect(osxSign.optionsForFile?.("/app", { platform: "darwin" })).toEqual({ + entitlements: "/entitlements/electron.plist", + hardenedRuntime: true, + }); + expect(options.osxNotarize).toEqual({ + appleApiKey: env.APPLE_API_KEY, + appleApiKeyId: env.APPLE_API_KEY_ID, + appleApiIssuer: env.APPLE_API_ISSUER, + }); + expect( + preserveDaemonSignature("/stage/Polaris.app/Contents/Resources/daemon/darwin-arm64/polaris") + ).toBe(true); + expect( + preserveDaemonSignature("/stage/Polaris.app/Contents/Resources/daemon/linux-x64/polaris") + ).toBe(true); + expect( + preserveDaemonSignature("/stage/Polaris.app/Contents/Frameworks/Electron Framework.framework") + ).toBe(false); + expect(preserveDaemonSignature("/stage/Polaris.app/Contents/Resources/daemon-other/tool")).toBe( + false + ); + }); + + test("archives only after signature and stapled ticket verification, preserving symlinks", () => { + const commands: ReadonlyArray[] = []; + + const zip = updateZip("/dist/Polaris.app", "0.1.0-rc.1", "/dist", signing, (argv) => { + commands.push(argv); + + return ""; + }); + + expect(zip).toBe(join("/dist", "Polaris-0.1.0-rc.1-arm64-mac.zip")); + expect(commands).toEqual([ + ["codesign", "--verify", "--deep", "--strict", "/dist/Polaris.app"], + ["xcrun", "stapler", "validate", "/dist/Polaris.app"], + ["ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", "/dist/Polaris.app", zip], + ]); + expect(() => + updateZip("/app", "0.1.0", "/dist", signing, () => { + throw new Error("invalid ticket"); + }) + ).toThrow("invalid ticket"); + commands.length = 0; + updateZip("/app", "0.1.0", "/dist", null, (argv) => { + commands.push(argv); + + return ""; + }); + expect(commands.map((argv) => argv[0])).toEqual(["ditto"]); + }); +}); diff --git a/apps/desktop/scripts/packaging/release.ts b/apps/desktop/scripts/packaging/release.ts new file mode 100644 index 00000000..df21869b --- /dev/null +++ b/apps/desktop/scripts/packaging/release.ts @@ -0,0 +1,143 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import type { Options, PackagerOsxSignOptions } from "@electron/packager"; +import { type Run, run } from "./command.ts"; + +const REQUIRED = [ + "APPLE_TEAM_ID", + "APPLE_API_KEY", + "APPLE_API_KEY_ID", + "APPLE_API_ISSUER", +] as const; + +const OPTIONAL = [ + "MACOS_CERT_P12", + "MACOS_CERT_PASSWORD", + "MACOS_SIGNING_IDENTITY", + "MACOS_KEYCHAIN", +]; + +export const signingCredentials = (env: Readonly>) => { + if (![...REQUIRED, ...OPTIONAL].some((name) => env[name]?.trim())) return null; + + const missing = REQUIRED.filter((name) => !env[name]?.trim()); + + if (missing.length > 0) + throw new Error(`incomplete signing credentials: missing ${missing.join(", ")}`); + + return { + teamId: env.APPLE_TEAM_ID!, + appleApiKey: env.APPLE_API_KEY!, + appleApiKeyId: env.APPLE_API_KEY_ID!, + appleApiIssuer: env.APPLE_API_ISSUER!, + identity: env.MACOS_SIGNING_IDENTITY, + keychain: env.MACOS_KEYCHAIN, + }; +}; + +export type SigningCredentials = NonNullable>; + +export type Signing = SigningCredentials & { readonly identity: string }; + +export const selectIdentity = (credentials: SigningCredentials, output: string): string => { + const matches = [ + ...output.matchAll(/\b([0-9A-Fa-f]{40}) "(Developer ID Application: [^"\n]+)"/g), + ].filter( + ([, hash, name]) => + name!.endsWith(`(${credentials.teamId})`) && + (!credentials.identity || credentials.identity === name || credentials.identity === hash) + ); + + if (matches.length !== 1) + throw new Error( + "expected one valid Developer ID Application identity for APPLE_TEAM_ID; select with MACOS_SIGNING_IDENTITY" + ); + + return matches[0]![1]!; +}; + +export const resolveSigning = ( + credentials: SigningCredentials | null, + execute: Run = run +): Signing | null => { + if (credentials === null) return null; + + if (process.platform !== "darwin") throw new Error("signing requires macOS"); + + if (!existsSync(credentials.appleApiKey)) + throw new Error("APPLE_API_KEY must be a path to the .p8 API key file"); + + const output = execute([ + "security", + "find-identity", + "-v", + "-p", + "codesigning", + ...(credentials.keychain ? [credentials.keychain] : []), + ]); + + return { ...credentials, identity: selectIdentity(credentials, output) }; +}; + +// Preserve the already signed tools and the manifest hashes used for remote Host installs. +export const preserveDaemonSignature = (path: string): boolean => + /\/Contents\/Resources\/daemon(?:\/|$)/.test(path); + +export const macSigningOptions = ( + signing: Signing | null, + entitlementsDir: string +): Pick => { + if (signing === null) return {}; + + const osxSign: PackagerOsxSignOptions = { + identity: signing.identity, + continueOnError: false, + ignore: preserveDaemonSignature, + optionsForFile: () => ({ + hardenedRuntime: true, + entitlements: join(entitlementsDir, "electron.plist"), + }), + }; + + return { + osxSign: signing.keychain ? { ...osxSign, keychain: signing.keychain } : osxSign, + osxNotarize: { + appleApiKey: signing.appleApiKey, + appleApiKeyId: signing.appleApiKeyId, + appleApiIssuer: signing.appleApiIssuer, + }, + }; +}; + +export const assertReleaseVersions = (desktop: string, daemon: string, reuse: boolean) => { + if (desktop !== daemon) + throw new Error(`release version mismatch: Desktop App ${desktop}, Daemon ${daemon}`); + + if (reuse) + throw new Error("--reuse-daemon cannot be used with --release; rebuild every release Daemon"); +}; + +export const daemonBuildCommand = (root: string, release: boolean): string[] => [ + process.execPath, + join(root, "scripts/build-daemon.ts"), + ...(release ? ["--release"] : []), +]; + +export const updateZip = ( + bundle: string, + version: string, + outputDir: string, + signing: Signing | null, + execute: Run = run +): string => { + if (signing !== null) { + execute(["codesign", "--verify", "--deep", "--strict", bundle]); + // @electron/notarize has already stapled the app before packager returns. + execute(["xcrun", "stapler", "validate", bundle]); + } + + const zip = join(outputDir, `Polaris-${version}-arm64-mac.zip`); + execute(["ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", bundle, zip]); + + return zip; +}; From 19cbf6a801c2e57eeb5dae8ee3cc9a3e75487d65 Mon Sep 17 00:00:00 2001 From: Lucas Doell Date: Fri, 2 Oct 2026 19:13:36 -0400 Subject: [PATCH 06/20] fix(desktop): isolate smoke Daemons from upgrade handoff --- apps/desktop/scripts/lib/daemon.test.ts | 32 +++++++++++++++++++ apps/desktop/scripts/lib/daemon.ts | 8 ++++- .../scripts/lib/fixtures/daemon-start.ts | 24 ++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/scripts/lib/daemon.test.ts create mode 100644 apps/desktop/scripts/lib/fixtures/daemon-start.ts diff --git a/apps/desktop/scripts/lib/daemon.test.ts b/apps/desktop/scripts/lib/daemon.test.ts new file mode 100644 index 00000000..57b56ae0 --- /dev/null +++ b/apps/desktop/scripts/lib/daemon.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +test("a fresh source Daemon ignores its parent's upgrade hand-off", () => { + const home = mkdtempSync(join(tmpdir(), "polaris-smoke-handoff-")); + + try { + const result = spawnSync("node", [join(import.meta.dir, "fixtures/daemon-start.ts"), home], { + env: { + ...process.env, + POLARIS_HANDOFF: JSON.stringify({ + listenerFd: 999_999, + fds: {}, + children: {}, + fromVersion: "0.0.0-test", + requestId: null, + }), + }, + encoding: "utf8", + timeout: 25_000, + }); + + expect(result.error).toBeUndefined(); + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); + } finally { + rmSync(home, { recursive: true, force: true }); + } +}, 30_000); diff --git a/apps/desktop/scripts/lib/daemon.ts b/apps/desktop/scripts/lib/daemon.ts index c0cd12cb..cc11c5c6 100644 --- a/apps/desktop/scripts/lib/daemon.ts +++ b/apps/desktop/scripts/lib/daemon.ts @@ -49,7 +49,13 @@ const testEnv = ( userHome: string | undefined, extra: Readonly> ) => { - const { CLAUDE_CONFIG_DIR: _claude, CODEX_HOME: _codex, ...env } = process.env; + // A fresh child does not inherit the listener described by a Daemon upgrade hand-off. + const { + CLAUDE_CONFIG_DIR: _claude, + CODEX_HOME: _codex, + POLARIS_HANDOFF: _handoff, + ...env + } = process.env; return { ...env, diff --git a/apps/desktop/scripts/lib/fixtures/daemon-start.ts b/apps/desktop/scripts/lib/fixtures/daemon-start.ts new file mode 100644 index 00000000..b7151369 --- /dev/null +++ b/apps/desktop/scripts/lib/fixtures/daemon-start.ts @@ -0,0 +1,24 @@ +import { once } from "node:events"; +import { connect } from "node:net"; +import { setTimeout as sleep } from "node:timers/promises"; +import { startDaemon } from "../daemon.ts"; + +const home = process.argv[2]; + +if (home === undefined) throw new Error("expected a temporary Daemon home"); + +const daemon = await startDaemon({ home, benchHarness: true }); + +try { + // Reconnect after the upgrade drain's grace period, when a stale descriptor used to exit. + await sleep(600); + const socket = connect(daemon.socketPath); + + try { + await once(socket, "connect"); + } finally { + socket.destroy(); + } +} finally { + await daemon.stop(); +} From a83489c153825bac73e84a94ca38d875d052ec40 Mon Sep 17 00:00:00 2001 From: Lucas Doell Date: Fri, 2 Oct 2026 19:13:58 -0400 Subject: [PATCH 07/20] docs(design): record owner approval of quiet daemon upgrades and Quit and update (ENG-256) The 2026-10-01 'Daemon upgraded' toast is superseded by the machine-bar caption, tooltip and Hosts row; the updater task removes it from code. --- .agents/skills/product-design/decision-log.md | 3 ++- DESIGN.md | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.agents/skills/product-design/decision-log.md b/.agents/skills/product-design/decision-log.md index 325ebbea..fd76c0ff 100644 --- a/.agents/skills/product-design/decision-log.md +++ b/.agents/skills/product-design/decision-log.md @@ -31,7 +31,7 @@ One line per accepted guidance change: `date | change | evidence`. 2026-10-01 | Plan Limit meters count down ("84% left", meter = share left, "<1% left" for a sliver), drawn as crisp discrete cells (Paper S2's 40 × 4px + 1px gaps), windows wrap to a second line instead of clipping; the picker hint counts down too | user feedback, round 5 (R5-limits) 2026-10-01 | Plan Limit forecast after CodexBar: pace marker cell in text-strong (taller by 1px each way, not a signal colour), reserve/deficit, "Runs out in" / "Lasts until reset" from the window's average rate, weekly "About N full 5-hour windows left · M until reset" from Daemon history; the picker hint gets a short form. Paper S2 has no forecast or marker | user request, round 6 (R6-forecast) 2026-10-01 | Harness picker: the newest four Models per Harness (newest of each family, Harness order, default always shown; the rest under "More models…"; Settings defaults use the same four); effort as a dither bar whose density and pace rise with the level; picks staged and sent once on close, Escape cancels | user request R6 (Codex-style effort bar, "top four" models) -2026-10-01 | The / menu never shows an empty list for a Host whose Daemon predates it: it says "Skills need a newer daemon on " with one action, Upgrade daemon (the Host's own upgrade), and follows it; any Host's upgrade is a toast ("Daemon upgraded") | user report R6 ("I don't see the skills working"): their Mac ran an older installed Daemon +2026-10-01 | The / menu never shows an empty list for a Host whose Daemon predates it: it says "Skills need a newer daemon on " with one action, Upgrade daemon (the Host's own upgrade), and follows it; any Host's upgrade is a toast ("Daemon upgraded") [toast superseded 2026-10-02: Daemon Upgrades are quiet, DESIGN.md Updates and daemon upgrades] | user report R6 ("I don't see the skills working"): their Mac ran an older installed Daemon 2026-10-01 | The / menu ranks by match quality, then the user's frecency of picks (ported from Sightline's Find); a bare / leads with "Recent"; local only, per Host, Workspace and Harness, bounded | user request R6 2026-10-01 | M2 Review states proposed in Paper (R3 pull requests, R4/R5 review checkout menu and states, R6 comment composer, R7 submit review, R8 agent session feedback; S5/S6 GitHub accounts and device-flow sign-in) and recorded in DESIGN.md Review and Settings as proposed | user request (ENG-185 resolution, ENG-217 map: PR list, account mapping, checkout states, comment and submit flow) 2026-10-01 | Reviewer settings page proposed (Paper S7): reviewer Harness/Model/Effort with per-host readiness (hosts that can't run it fall back to rules only), when it runs (PR on open, agent session on open/accept, ask first over 2,000 lines), rules and risk memory summary, 7-day usage line | user request after M2 Review mockups @@ -44,3 +44,4 @@ One line per accepted guidance change: `date | change | evidence`. 2026-10-02 | M3 explorer built from E1: compact folder chains, deleted files kept struck through, the hand on the nearest visible folder, Changes as one flat list (name then folder), three agents then "N more", Delete to the trash without asking (a dialog only where the Host has no trash), and a Worktree or Review Checkout as the explorer's root with "Back to {workspace}". "Agents in" and ages use `text-subtle` where E1 had `text-faint` (rule/faint-is-not-content) | M3-EXPLORER build; spec §2 and §5 2026-10-02 | M3.1 Editor language tooling planned: lazy per-host managed installs with developer-owned prerequisites and Workspace trust; Settings configures composed providers and format-on-save (on, failure saves with error toast); multi-file refactors preview into drafts with guarded resource operations; familiar language assistance and GFM preview via Shift+Cmd+V, Host-relative media and remembered external-image consent | owner answers Q1-Q25 in grill-with-docs; docs/specs/editor-language-tooling-m3.1.md; no implementation or mockups 2026-10-02 | Updates and Daemon Upgrades proposed (Paper Updates page U1–U6): "Restart to update" only in the app menu (plus "Quit and update" on ⌘Q), Settings → About and the K menu, never a toast, badge or dialog; About lists what each update check sends and drops the install ID when checks are off; automatic Daemon Upgrades are quiet (machine bar caption and tooltip, Hosts row), replacing the 2026-10-01 "Daemon upgraded" toast; copy says update for the app and upgrade for daemons | ENG-256 brief; CONTEXT.md Update / Daemon Upgrade; docs/adr/0017 +2026-10-02 | Owner approved: the "Daemon upgraded" toast is superseded by the quiet machine-bar caption, tooltip and Hosts row (the updater task removes the toast from code), and the app menu keeps "Quit and update" while an update is ready | owner answers to update-design questions upgrade-toast and quit-and-update, via the Lead diff --git a/DESIGN.md b/DESIGN.md index 16cec73a..76825e23 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -612,12 +612,12 @@ Settings replaces the three zones inside the one main window (no separate window ### Updates and daemon upgrades -An Update is the Desktop App replacing itself with a newer Release; a Daemon Upgrade is a host's daemon replaced by the build the app carries (CONTEXT.md). Copy keeps the two words apart, lowercase: "update" for the app, "upgrade" for daemons. Mockups: Updates page, U1–U6 (proposed). +An Update is the Desktop App replacing itself with a newer Release; a Daemon Upgrade is a host's daemon replaced by the build the app carries (CONTEXT.md). Copy keeps the two words apart, lowercase: "update" for the app, "upgrade" for daemons. Mockups: Updates page, U1–U6 (proposed; the quiet upgrades and "Quit and update" approved by the owner, 2026-10-02). - **Never interrupts.** An update downloads in the background and waits. There is no toast, badge, dot, dialog or Dock bounce, and nothing appears over a running Turn: the user finds it where they already look for the app's own state, and acts when they choose. -- **"Restart to update" in three places, all at once.** The app menu, under About Polaris: "Restart to update to 0.5.0" (the same slot reads "Check for updates…" otherwise), and Quit reads "Quit and update" (⌘Q installs on the way out; closing the window still only hides Polaris). Settings → About: the ready row, with the view's one primary button, and the nav foot's caption "0.4.0 · update ready". The K menu: an action "Restart to update" ("Polaris 0.5.0 · agent sessions keep working"), found by "update", "restart" or "upgrade", and listed last under Actions on an empty field; "Check for updates" and "Settings: About" sit beside it. The menu bar star's menu stays as it is. +- **"Restart to update" in three places, all at once.** The app menu, under About Polaris: "Restart to update to 0.5.0" (the same slot reads "Check for updates…" otherwise), and Quit reads "Quit and update" (⌘Q installs on the way out; closing the window still only hides Polaris; kept by the owner, 2026-10-02). Settings → About: the ready row, with the view's one primary button, and the nav foot's caption "0.4.0 · update ready". The K menu: an action "Restart to update" ("Polaris 0.5.0 · agent sessions keep working"), found by "update", "restart" or "upgrade", and listed last under Actions on an empty field; "Check for updates" and "Settings: About" sit beside it. The menu bar star's menu stays as it is. - **A restart keeps sessions.** Agent sessions run in their daemons, so a restart never stops a Turn; the copy says "agent sessions keep working", never asks to confirm, and never counts down. -- **Quiet daemon upgrades (U5, U6).** After an automatic Daemon Upgrade the host says so where hosts already speak: the machine bar's caption under the host's name becomes "Upgraded to 0.5.0" (in place of its workspace count) for an hour or until the user opens that host, and its tooltip reads "Daemon upgraded to 0.5.0 · 3m ago" over "From 0.4.0, to match this Mac. Agent sessions kept working."; Settings → Hosts keeps "Upgraded to 0.5.0 · 3m ago" with the pixel check for a day. No toast, for automatic or user-started upgrades: one release upgrades every host, and a toast per host would be the noise this avoids. In the workspace bar (dark Orchestrator, no mockup) the host's label carries the same tooltip. +- **Quiet daemon upgrades (U5, U6).** After an automatic Daemon Upgrade the host says so where hosts already speak: the machine bar's caption under the host's name becomes "Upgraded to 0.5.0" (in place of its workspace count) for an hour or until the user opens that host, and its tooltip reads "Daemon upgraded to 0.5.0 · 3m ago" over "From 0.4.0, to match this Mac. Agent sessions kept working."; Settings → Hosts keeps "Upgraded to 0.5.0 · 3m ago" with the pixel check for a day. No toast, for automatic or user-started upgrades: one release upgrades every host, and a toast per host would be the noise this avoids. This supersedes the 2026-10-01 decision that any Host's upgrade is a "Daemon upgraded" toast (owner, 2026-10-02); the toast still in code (`upgradeNotes` in `apps/desktop/src/renderer/features/machines/model.ts`, `main/machines/README.md`) goes with the updater work (ENG-261). In the workspace bar (dark Orchestrator, no mockup) the host's label carries the same tooltip. ### Onboarding From 4770298affdc392fa3fce78f881ebadc4a37571d Mon Sep 17 00:00:00 2001 From: Lucas Doell Date: Fri, 2 Oct 2026 19:18:23 -0400 Subject: [PATCH 08/20] feat(site): marketing site in apps/site (ENG-257) Next.js 16 (App Router, Turbopack) for Vercel with root directory apps/site, built from the Paper "Marketing site" page: S1 desktop dark, S2 light (system appearance) and S3 mobile, on @polaris/ui's tokens plus the site palette. Product shots, scenes, pixel icons and washes are imported from design/assets, never copied. The repo is public, so the "Source opens soon" notify form becomes a real source link: "View source" buttons and a repository card with the clone line. "Download for macOS" goes to /download/mac (ENG-259). On phones the primary action drafts the download link to the visitor's own address. The site joins typecheck, test, lint and turbo build. Next pulls in caniuse-lite (CC-BY-4.0) and sharp's prebuilt libvips (LGPL-3.0), recorded as licence exceptions with reasons; THIRD_PARTY_NOTICES.md regenerated. AGENTS.md gains the apps/site row; DESIGN.md's Marketing site section drops the notify form and records the mobile email draft and breakpoints. --- AGENTS.md | 1 + DESIGN.md | 2 +- THIRD_PARTY_NOTICES.md | 1734 ++++++++++++++++++- apps/site/.gitignore | 3 + apps/site/.oxlintrc.json | 12 + apps/site/AGENTS.md | 9 + apps/site/CLAUDE.md | 1 + apps/site/README.md | 16 + apps/site/app/globals.css | 247 +++ apps/site/app/layout.tsx | 40 + apps/site/app/page.tsx | 25 + apps/site/components/actions.tsx | 77 + apps/site/components/email-download.test.ts | 32 + apps/site/components/email-download.tsx | 54 + apps/site/components/features.tsx | 153 ++ apps/site/components/final-cta.tsx | 30 + apps/site/components/footer.tsx | 86 + apps/site/components/harness-strip.tsx | 41 + apps/site/components/hero.tsx | 49 + apps/site/components/hosts.tsx | 144 ++ apps/site/components/icons.tsx | 88 + apps/site/components/links.ts | 28 + apps/site/components/mobile-menu.tsx | 48 + apps/site/components/nav.tsx | 60 + apps/site/components/open-source.tsx | 96 + apps/site/components/pixel.tsx | 36 + apps/site/components/section-header.tsx | 21 + apps/site/components/shot.tsx | 88 + apps/site/next.config.ts | 17 + apps/site/package.json | 27 + apps/site/postcss.config.mjs | 5 + apps/site/tsconfig.json | 20 + apps/site/types.d.ts | 2 + bun.lock | 109 ++ scripts/license-exceptions.json | 6 + turbo.json | 4 + 36 files changed, 3339 insertions(+), 72 deletions(-) create mode 100644 apps/site/.gitignore create mode 100644 apps/site/.oxlintrc.json create mode 100644 apps/site/AGENTS.md create mode 100644 apps/site/CLAUDE.md create mode 100644 apps/site/README.md create mode 100644 apps/site/app/globals.css create mode 100644 apps/site/app/layout.tsx create mode 100644 apps/site/app/page.tsx create mode 100644 apps/site/components/actions.tsx create mode 100644 apps/site/components/email-download.test.ts create mode 100644 apps/site/components/email-download.tsx create mode 100644 apps/site/components/features.tsx create mode 100644 apps/site/components/final-cta.tsx create mode 100644 apps/site/components/footer.tsx create mode 100644 apps/site/components/harness-strip.tsx create mode 100644 apps/site/components/hero.tsx create mode 100644 apps/site/components/hosts.tsx create mode 100644 apps/site/components/icons.tsx create mode 100644 apps/site/components/links.ts create mode 100644 apps/site/components/mobile-menu.tsx create mode 100644 apps/site/components/nav.tsx create mode 100644 apps/site/components/open-source.tsx create mode 100644 apps/site/components/pixel.tsx create mode 100644 apps/site/components/section-header.tsx create mode 100644 apps/site/components/shot.tsx create mode 100644 apps/site/next.config.ts create mode 100644 apps/site/package.json create mode 100644 apps/site/postcss.config.mjs create mode 100644 apps/site/tsconfig.json create mode 100644 apps/site/types.d.ts diff --git a/AGENTS.md b/AGENTS.md index efcda9dd..3b190dcd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ Polaris is an IDE and agent orchestrator: a Bun Daemon per Host, an Electron Des | `packages/bench` | Benchmarks: the real Daemon under scripted load, a process-tree memory/CPU sampler, baselines and comparisons. | | `packages/spec` | The Quint spec of commits, streams, Client feeds and approvals, and its checks (see Verification). | | `apps/daemon` | The Daemon (`polaris` binary): event store, Harness drivers, transport, files, git, terminals, user service. | +| `apps/site` | The marketing site (Next.js on Vercel, root directory `apps/site`), built from the Paper "Marketing site" page on `@polaris/ui`'s tokens; the download and update-feed routes live under `app/` (see its README). | | `apps/desktop` | The Electron Desktop App: the Client runtime in the main process, a typed IPC bridge, a React renderer on `@polaris/ui` (see its README). | | `packages/ui` | The design system in code (`@polaris/ui`): DESIGN.md's tokens on Tailwind v4, restyled shadcn/ui, the Polaris primitives, and a gallery (`bun run --cwd packages/ui gallery`). | | `packages/lint-config` | The shared oxlint config every workspace extends, the shared oxfmt style (`oxfmt.json`), and the custom plugins (`polaris/no-long-comment`, vendored anti-slop, the SonarJS cognitive-complexity wrapper). Read its `README.md` before touching lint rules. | diff --git a/DESIGN.md b/DESIGN.md index 45a90d90..8f763d56 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -310,7 +310,7 @@ The Desktop App is Electron (Chromium 152, React). CSS effects are available: sh - **The twinkle:** on launch only, the star's core brightens for two frames. The mark never animates anywhere else. - **Brand deck:** the "Brand deck" page of the Paper file (12 slides: cover, idea, principles, voice, mark, logo, app icon, colour, typography, pixel world, icons and motion, applications). - Draw the mark on whole pixels only; never rotate, outline, or anti-alias it. -- **Marketing site:** the "Marketing site" page of the Paper file (S1, desktop, dark, a draft). It uses the brand deck's system: Night ground, the night-sky scene in the hero and the closing CTA only, SF Pro Regular headlines with tight tracking, mono section kickers (`01 — Supervise`), and real product shots exported from accepted artboards (`design/assets/site/`), never redrawn UI. One primary action on every screen, "Download for macOS", as a Snow button; the secondary action is always the source. Starlight stays off buttons and links here too (rule/starlight-is-rare), and signal colours appear only inside product shots. Until the repo is public, every source link reads "Source opens soon" and leads to a one-email notify form. Copy names what Polaris does, not the hardware it runs on: hosts are "this Mac" or "any machine you can reach over SSH". The footer credits lux.dev LLC ("An open source project by lux.dev", "© 2026 lux.dev LLC"). rule/scene-text-contrast holds on the site too: the nav sits on a solid Night bar (~95%) with links in `#C9D0E0`, never directly on the sky, and buttons over a scene are opaque. S2 is the light version: the dawn scene in the hero (faded into `bg-light` above the product shot) and the closing CTA, light product shots, ink primary buttons, the blue colourway of the mark, and light dither and wash variants. S3 is mobile (390, dark): one column, shots bleed off the right edge, and because a phone cannot install a Mac app the primary action becomes "Email me the Mac download" (email field plus button) instead of Download. +- **Marketing site:** the "Marketing site" page of the Paper file (S1 desktop dark, S2 light, S3 mobile), built in `apps/site`. It uses the brand deck's system: Night ground, the night-sky scene in the hero and the closing CTA only, SF Pro Regular headlines with tight tracking, section kickers (`01 — Supervise`: the pixel star, SF Pro at 14px with 0.02em tracking, then a hairline to the edge), and real product shots exported from accepted artboards (`design/assets/site/`), never redrawn UI. One primary action on every screen, "Download for macOS" (to `/download/mac`, which redirects to the latest Release's DMG), as a Snow button; the secondary action is always the source, "View source" with the GitHub mark, linking to github.com/luxdotdev/polaris. Starlight stays off buttons and links here too (rule/starlight-is-rare), and signal colours appear only inside product shots. The repo is public: the "03 — Yours" card names the repository, says every line is on GitHub, shows the `git clone` line and a "View source" button; there is no notify form. Copy names what Polaris does, not the hardware it runs on: hosts are "this Mac" or "any machine you can reach over SSH". The footer credits lux.dev LLC ("An open source project by lux.dev", "© 2026 lux.dev LLC"). rule/scene-text-contrast holds on the site too: the nav sits on a solid Night bar (~95%) with links in `#C9D0E0`, never directly on the sky, and buttons over a scene are opaque. S2 is the light version: the dawn scene in the hero (faded into `bg-light` above the product shot) and the closing CTA, light product shots, ink primary buttons, the blue colourway of the mark, and light dither and wash variants. S3 is mobile (390, dark): one column, shots bleed off the right edge, and because a phone cannot install a Mac app the primary action becomes "Email me the Mac download" (email field plus button) instead of Download; it opens a draft to that address in the visitor's mail app, so nothing is sent to or stored by Polaris. The site follows the system appearance (S1 dark, S2 light; phones get S3's layout in either), lays out S1/S2 from 1024px and S3 below, and shows the email form only below 768px. ## Colors diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index e89dcecf..12670788 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -13,7 +13,10 @@ Generated by `bun run licenses` from the installed production dependencies of th | @ast-grep/lang-rust | 0.0.7 | ISC | | | @ast-grep/napi | 0.45.3 | MIT | https://github.com/ast-grep/ast-grep | | @ast-grep/setup-lang | 0.0.6 | ISC | | +| @babel/helper-string-parser | 7.29.7 | MIT | https://github.com/babel/babel | +| @babel/helper-validator-identifier | 7.29.7 | MIT | https://github.com/babel/babel | | @babel/runtime | 7.29.7 | MIT | https://github.com/babel/babel | +| @babel/types | 7.29.8 | MIT | https://github.com/babel/babel | | @braintree/sanitize-url | 7.1.2 | MIT | https://github.com/braintree/sanitize-url | | @chevrotain/types | 11.1.2 | Apache-2.0 | git://github.com/Chevrotain/chevrotain | | @codemirror/autocomplete | 6.20.3 | MIT | https://code.haverbeke.berlin/codemirror/autocomplete | @@ -48,6 +51,7 @@ Generated by `bun run licenses` from the installed production dependencies of th | @hono/node-server | 2.1.1 | MIT | https://github.com/honojs/node-server | | @iconify/types | 2.0.0 | MIT | https://github.com/iconify/iconify | | @iconify/utils | 3.1.7 | MIT | https://github.com/iconify/iconify | +| @img/colour | 1.1.0 | MIT | https://github.com/lovell/colour | | @lexical/clipboard | 0.52.0 | MIT | https://github.com/facebook/lexical | | @lexical/dragon | 0.52.0 | MIT | https://github.com/facebook/lexical | | @lexical/extension | 0.52.0 | MIT | https://github.com/facebook/lexical | @@ -75,6 +79,7 @@ Generated by `bun run licenses` from the installed production dependencies of th | @marijn/find-cluster-break | 1.0.4 | MIT | https://code.haverbeke.berlin/marijn/find-cluster-break | | @mermaid-js/parser | 1.2.1 | MIT | https://github.com/mermaid-js/mermaid | | @modelcontextprotocol/sdk | 1.30.1 | MIT | https://github.com/modelcontextprotocol/typescript-sdk | +| @next/env | 16.3.8 | MIT | https://github.com/vercel/next.js | | @pierre/diffs | 1.5.1 | apache-2.0 | | | @pierre/theme | 2.0.0 | apache-2.0 | https://github.com/pierrecomputer/pierre | | @pierre/theming | 1.0.1 | apache-2.0 | | @@ -157,6 +162,7 @@ Generated by `bun run licenses` from the installed production dependencies of th | @streamdown/code | 2.0.0 | Apache-2.0 | https://github.com/vercel/streamdown | | @streamdown/math | 1.0.3 | Apache-2.0 | https://github.com/vercel/streamdown | | @streamdown/mermaid | 1.0.3 | Apache-2.0 | https://github.com/vercel/streamdown | +| @swc/helpers | 0.5.23 | Apache-2.0 | https://github.com/swc-project/swc | | @tanstack/react-virtual | 3.14.13 | MIT | https://github.com/TanStack/virtual | | @tanstack/virtual-core | 3.17.11 | MIT | https://github.com/TanStack/virtual | | @types/d3 | 7.4.3 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped | @@ -215,17 +221,21 @@ Generated by `bun run licenses` from the installed production dependencies of th | ajv | 8.20.0 | MIT | ajv-validator/ajv | | ajv-formats | 3.0.1 | MIT | https://github.com/ajv-validator/ajv-formats | | aria-hidden | 1.2.6 | MIT | https://github.com/theKashey/aria-hidden | +| babel-plugin-react-compiler | 1.0.0 | MIT | https://github.com/facebook/react | | bail | 2.0.2 | MIT | wooorm/bail | +| baseline-browser-mapping | 2.11.26 | Apache-2.0 | https://github.com/web-platform-dx/baseline-browser-mapping | | body-parser | 2.3.0 | MIT | expressjs/body-parser | | bytes | 3.1.2 | MIT | visionmedia/bytes.js | | call-bind-apply-helpers | 1.0.2 | MIT | https://github.com/ljharb/call-bind-apply-helpers | | call-bound | 1.0.4 | MIT | https://github.com/ljharb/call-bound | +| caniuse-lite | 1.0.30001813 | CC-BY-4.0 (exception) | browserslist/caniuse-lite | | ccount | 2.0.1 | MIT | wooorm/ccount | | character-entities | 2.0.2 | MIT | wooorm/character-entities | | character-entities-html4 | 2.1.0 | MIT | wooorm/character-entities-html4 | | character-entities-legacy | 3.0.0 | MIT | wooorm/character-entities-legacy | | character-reference-invalid | 2.0.1 | MIT | wooorm/character-reference-invalid | | class-variance-authority | 0.7.1 | Apache-2.0 | https://github.com/joe-bell/cva | +| client-only | 0.0.1 | MIT | https://reactjs.org/ | | clsx | 2.1.1 | MIT | lukeed/clsx | | cmdk | 1.1.1 | MIT | https://github.com/pacocoursey/cmdk | | comma-separated-tokens | 2.0.3 | MIT | wooorm/comma-separated-tokens | @@ -287,6 +297,7 @@ Generated by `bun run licenses` from the installed production dependencies of th | delaunator | 5.1.0 | ISC | https://github.com/mapbox/delaunator | | depd | 2.0.0 | MIT | dougwilson/nodejs-depd | | dequal | 2.0.3 | MIT | lukeed/dequal | +| detect-libc | 2.1.2 | Apache-2.0 | git://github.com/lovell/detect-libc | | detect-node-es | 1.1.0 | MIT | https://github.com/thekashey/detect-node | | devlop | 1.1.0 | MIT | wooorm/devlop | | diff | 9.0.0 | BSD-3-Clause | https://github.com/kpdecker/jsdiff | @@ -425,7 +436,9 @@ Generated by `bun run licenses` from the installed production dependencies of th | mime-db | 1.54.0 | MIT | jshttp/mime-db | | mime-types | 3.0.2 | MIT | jshttp/mime-types | | ms | 2.1.3 | MIT | vercel/ms | +| nanoid | 3.3.19 | MIT | ai/nanoid | | negotiator | 1.1.0 | MIT | jshttp/negotiator | +| next | 16.3.8 | MIT | vercel/next.js | | object-assign | 4.1.1 | MIT | sindresorhus/object-assign | | object-inspect | 1.13.4 | MIT | git://github.com/inspect-js/object-inspect | | on-finished | 2.4.1 | MIT | jshttp/on-finished | @@ -439,9 +452,11 @@ Generated by `bun run licenses` from the installed production dependencies of th | path-data-parser | 0.1.0 | MIT | https://github.com/pshihn/path-data-parser | | path-key | 3.1.1 | MIT | sindresorhus/path-key | | path-to-regexp | 8.4.2 | MIT | https://github.com/pillarjs/path-to-regexp | +| picocolors | 1.1.1 | ISC | alexeyraspopov/picocolors | | pkce-challenge | 5.0.1 | MIT | https://github.com/crouchcd/pkce-challenge | | points-on-curve | 0.2.0 | MIT | https://github.com/pshihn/bezier-points | | points-on-path | 0.2.1 | MIT | https://github.com/pshihn/points-on-path | +| postcss | 8.5.23 | MIT | postcss/postcss | | property-information | 7.2.0 | MIT | wooorm/property-information | | proxy-addr | 2.0.8 | MIT | jshttp/proxy-addr | | qs | 6.16.0 | BSD-3-Clause | https://github.com/ljharb/qs | @@ -473,9 +488,11 @@ Generated by `bun run licenses` from the installed production dependencies of th | rw | 1.3.3 | BSD-3-Clause | http://github.com/mbostock/rw | | safer-buffer | 2.1.2 | MIT | https://github.com/ChALkeR/safer-buffer | | scheduler | 0.28.0 | MIT | https://github.com/react/react | +| semver | 7.8.5 | ISC | https://github.com/npm/node-semver | | send | 1.2.1 | MIT | pillarjs/send | | serve-static | 2.2.1 | MIT | expressjs/serve-static | | setprototypeof | 1.2.0 | ISC | https://github.com/wesleytodd/setprototypeof | +| sharp | 0.35.5 | Apache-2.0 | https://github.com/lovell/sharp | | shebang-command | 2.0.0 | MIT | kevva/shebang-command | | shebang-regex | 3.0.0 | MIT | sindresorhus/shebang-regex | | shiki | 4.4.3 | MIT | https://github.com/shikijs/shiki | @@ -484,6 +501,7 @@ Generated by `bun run licenses` from the installed production dependencies of th | side-channel-map | 1.0.1 | MIT | https://github.com/ljharb/side-channel-map | | side-channel-weakmap | 1.0.2 | MIT | https://github.com/ljharb/side-channel-weakmap | | sonner | 2.0.8 | MIT | https://github.com/emilkowalski/sonner | +| source-map-js | 1.2.1 | BSD-3-Clause | 7rulnik/source-map-js | | space-separated-tokens | 2.0.2 | MIT | wooorm/space-separated-tokens | | standardwebhooks | 1.1.1 | MIT | https://github.com/standard-webhooks/standard-webhooks | | statuses | 2.0.2 | MIT | jshttp/statuses | @@ -493,6 +511,7 @@ Generated by `bun run licenses` from the installed production dependencies of th | style-mod | 4.1.4 | MIT | https://code.haverbeke.berlin/marijn/style-mod | | style-to-js | 1.1.21 | MIT | https://github.com/remarkablemark/style-to-js | | style-to-object | 1.0.14 | MIT | https://github.com/remarkablemark/style-to-object | +| styled-jsx | 5.1.6 | MIT | vercel/styled-jsx | | stylis | 4.4.0 | MIT | https://github.com/thysultan/stylis.js | | tailwind-merge | 3.7.0 | MIT | https://github.com/dcastil/tailwind-merge | | tailwindcss | 4.3.3 | MIT | https://github.com/tailwindlabs/tailwindcss | @@ -537,6 +556,7 @@ Generated by `bun run licenses` from the installed production dependencies of th ## Licence exceptions - **@anthropic-ai/claude-agent-sdk**: Anthropic terms; tolerated as in T3 Code; recheck before public release. +- **caniuse-lite**: CC-BY-4.0 browser-support data (attribution kept in THIRD_PARTY_NOTICES.md), pulled in by next for apps/site; data, not code, and never shipped in the Desktop App or Daemon. - **khroma**: MIT per its bundled license file (khroma@2.1.0/license); its package.json omits the licence field. Pulled in by mermaid (via @streamdown/mermaid). ## Licence texts @@ -745,6 +765,60 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### @babel/helper-string-parser@7.29.7 + +```text +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + +### @babel/helper-validator-identifier@7.29.7 + +```text +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + ### @babel/runtime@7.29.7 ```text @@ -772,6 +846,33 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### @babel/types@7.29.8 + +```text +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + ### @braintree/sanitize-url@7.1.2 ```text @@ -1812,6 +1913,93 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### @img/colour@1.1.0 + +```text +# Licensing + +## color + +Copyright (c) 2012 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## color-convert + +Copyright (c) 2011-2016 Heather Arthur . +Copyright (c) 2016-2021 Josh Junon . + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## color-string + +Copyright (c) 2011 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## color-name + +The MIT License (MIT) +Copyright (c) 2015 Dmitry Ivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + ### @lexical/clipboard@0.52.0 ```text @@ -2514,6 +2702,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### @next/env@16.3.8 + +Licensed under MIT (no licence file shipped). + ### @pierre/diffs@1.5.1 ```text @@ -5174,6 +5366,212 @@ See the License for the specific language governing permissions and limitations under the License. ``` +### @swc/helpers@0.5.23 + +```text +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2024 SWC contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + ### @tanstack/react-virtual@3.14.13 ```text @@ -6673,6 +7071,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### babel-plugin-react-compiler@1.0.0 + +Licensed under MIT (no licence file shipped). + ### bail@2.0.2 ```text @@ -6700,60 +7102,266 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -### body-parser@2.3.0 +### baseline-browser-mapping@2.11.26 ```text -(The MIT License) +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Copyright (c) 2014 Jonathan Ong -Copyright (c) 2014-2015 Douglas Christopher Wilson + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: + 1. Definitions. -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -### bytes@3.1.2 + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -```text -(The MIT License) + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -Copyright (c) 2012-2014 TJ Holowaychuk -Copyright (c) 2015 Jed Watson + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +``` + +### body-parser@2.3.0 + +```text +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + +### bytes@3.1.2 + +```text +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ### call-bind-apply-helpers@1.0.2 @@ -6808,6 +7416,406 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### caniuse-lite@1.0.30001813 + +```text +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. +``` + ### ccount@2.0.1 ```text @@ -7138,6 +8146,10 @@ Apache License limitations under the License. ``` +### client-only@0.0.1 + +Licensed under MIT (no licence file shipped). + ### clsx@2.1.1 ```text @@ -8533,42 +9545,248 @@ distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + +### dequal@2.0.3 + +```text +The MIT License (MIT) + +Copyright (c) Luke Edwards (lukeed.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` + +### detect-libc@2.1.2 + +```text +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -### dequal@2.0.3 + END OF TERMS AND CONDITIONS -```text -The MIT License (MIT) + APPENDIX: How to apply the Apache License to your work. -Copyright (c) Luke Edwards (lukeed.com) + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Copyright {yyyy} {name of copyright owner} -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ``` ### detect-node-es@1.1.0 @@ -12893,6 +14111,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### nanoid@3.3.19 + +```text +The MIT License (MIT) + +Copyright 2017 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + ### negotiator@1.1.0 ```text @@ -12922,6 +14165,34 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### next@16.3.8 + +```text +The MIT License (MIT) + +Copyright (c) 2025 Vercel, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +Per-platform builds published with this package: `@next/swc-darwin-arm64@16.3.8`, `@next/swc-darwin-x64@16.3.8`, `@next/swc-linux-arm64-gnu@16.3.8`, `@next/swc-linux-arm64-musl@16.3.8`, `@next/swc-linux-x64-gnu@16.3.8`, `@next/swc-linux-x64-musl@16.3.8`, `@next/swc-win32-arm64-msvc@16.3.8`, `@next/swc-win32-x64-msvc@16.3.8`. + ### object-assign@4.1.1 ```text @@ -13245,6 +14516,26 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### picocolors@1.1.1 + +```text +ISC License + +Copyright (c) 2021-2024 Oleksii Raspopov, Kostiantyn Denysov, Anton Verinov + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +``` + ### pkce-challenge@5.0.1 ```text @@ -13323,6 +14614,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### postcss@8.5.23 + +```text +The MIT License (MIT) + +Copyright 2013 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + ### property-information@7.2.0 ```text @@ -14082,6 +15398,26 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### semver@7.8.5 + +```text +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +``` + ### send@1.2.1 ```text @@ -14158,6 +15494,204 @@ OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ``` +### sharp@0.35.5 + +```text +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +``` + +Per-platform builds published with this package: `@img/sharp-darwin-arm64@0.35.5`, `@img/sharp-darwin-x64@0.35.5`, `@img/sharp-freebsd-wasm32@0.35.5`, `@img/sharp-libvips-darwin-arm64@1.3.4`, `@img/sharp-libvips-darwin-x64@1.3.4`, `@img/sharp-libvips-linux-arm64@1.3.4`, `@img/sharp-libvips-linux-arm@1.3.4`, `@img/sharp-libvips-linux-ppc64@1.3.4`, `@img/sharp-libvips-linux-riscv64@1.3.4`, `@img/sharp-libvips-linux-s390x@1.3.4`, `@img/sharp-libvips-linux-x64@1.3.4`, `@img/sharp-libvips-linuxmusl-arm64@1.3.4`, `@img/sharp-libvips-linuxmusl-x64@1.3.4`, `@img/sharp-linux-arm64@0.35.5`, `@img/sharp-linux-arm@0.35.5`, `@img/sharp-linux-ppc64@0.35.5`, `@img/sharp-linux-riscv64@0.35.5`, `@img/sharp-linux-s390x@0.35.5`, `@img/sharp-linux-x64@0.35.5`, `@img/sharp-linuxmusl-arm64@0.35.5`, `@img/sharp-linuxmusl-x64@0.35.5`, `@img/sharp-webcontainers-wasm32@0.35.5`, `@img/sharp-win32-arm64@0.35.5`, `@img/sharp-win32-ia32@0.35.5`, `@img/sharp-win32-x64@0.35.5`. + ### shebang-command@2.0.0 ```text @@ -14343,6 +15877,38 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### source-map-js@1.2.1 + +```text +Copyright (c) 2009-2011, Mozilla Foundation and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the names of the Mozilla Foundation nor the names of project + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + ### space-separated-tokens@2.0.2 ```text @@ -14528,6 +16094,32 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### styled-jsx@5.1.6 + +```text +MIT License + +Copyright (c) 2016-present Vercel, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + ### stylis@4.4.0 ```text diff --git a/apps/site/.gitignore b/apps/site/.gitignore new file mode 100644 index 00000000..65aed889 --- /dev/null +++ b/apps/site/.gitignore @@ -0,0 +1,3 @@ +.next/ +next-env.d.ts +.vercel/ diff --git a/apps/site/.oxlintrc.json b/apps/site/.oxlintrc.json new file mode 100644 index 00000000..a79dcd94 --- /dev/null +++ b/apps/site/.oxlintrc.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../node_modules/oxlint/configuration_schema.json", + "extends": ["../../packages/lint-config/oxlint.json"], + "plugins": ["typescript", "oxc", "react"], + "ignorePatterns": [".next/**", "next-env.d.ts"], + "rules": { + "react/rules-of-hooks": "error", + "react/exhaustive-deps": "error", + "react/jsx-key": "error", + "react/no-danger": "error" + } +} diff --git a/apps/site/AGENTS.md b/apps/site/AGENTS.md new file mode 100644 index 00000000..643577df --- /dev/null +++ b/apps/site/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/apps/site/CLAUDE.md b/apps/site/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/apps/site/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/apps/site/README.md b/apps/site/README.md new file mode 100644 index 00000000..0e38e0f8 --- /dev/null +++ b/apps/site/README.md @@ -0,0 +1,16 @@ +# @polaris/site + +The marketing site at polaris.lux.dev: Next.js (App Router, Turbopack) on Vercel. + +- **Design:** the Paper "Marketing site" page (S1 desktop dark, S2 light, S3 mobile) and DESIGN.md's Marketing site section. Colours are `@polaris/ui` tokens plus the site palette in `app/globals.css`; the site follows the system appearance. +- **Assets:** product shots, scenes, pixel icons and washes are imported straight from `design/assets/`, never copied or redrawn. `next.config.ts` sets the Turbopack and tracing roots to the monorepo so those imports resolve. +- **Routes:** `/` is static. `/download/mac` and the update feed (`app/download`, `app/api`) are built in ENG-259. + +```sh +bun run --cwd apps/site dev # http://localhost:3000 +bun run --cwd apps/site build +``` + +**Vercel:** root directory `apps/site`, framework Next.js, with "Include files outside the root directory" on (the default for monorepos), since the site reads `packages/ui` and `design/assets`. + +`next dev` writes the `AGENTS.md` here (Next's agent rules); keep it committed. diff --git a/apps/site/app/globals.css b/apps/site/app/globals.css new file mode 100644 index 00000000..d6d9546a --- /dev/null +++ b/apps/site/app/globals.css @@ -0,0 +1,247 @@ +/* The site: @polaris/ui's tokens plus the marketing palette of the Paper "Marketing site" page. */ +@import "@polaris/ui/styles.css"; + +@source "../app"; +@source "../components"; + +/* S1 (dark) and S2 (light) colours as light-dark() pairs; @polaris/ui flips color-scheme. */ +@theme static { + --color-site-ground: light-dark(#fbfbfc, #0a0d1a); + --color-site-bar: light-dark(#fbfbfcf2, #0a0d1af2); + --color-site-link: light-dark(#333438, #c9d0e0); + --color-site-copy: light-dark(#333438, #dde3f0); + --color-site-muted: light-dark(#5e6370, #9aa3bd); + --color-site-kicker: light-dark(#6b707c, #8a93ad); + --color-site-rule: light-dark(#0000001a, #bcd3ff1a); + --color-site-card: light-dark(#ffffff, #10142a); + --color-site-row: light-dark(#f7f7f9, #ffffff06); + --color-site-tile: light-dark(#ededef, #1a1f38); + --color-site-footer: light-dark(#f3f3f5, #070912); + --color-site-pill: light-dark(#ffffffcc, #0a0d1acc); + --color-site-pill-border: light-dark(#4f82e840, #bcd3ff29); + --color-site-field: light-dark(#ffffff, #0a0d1a); + --color-site-field-border: light-dark(#00000024, #bcd3ff33); + --color-site-primary: light-dark(#17181a, #f4f5f7); + --color-site-primary-hover: light-dark(#2b2c31, #ffffff); + --color-site-on-primary: light-dark(#ffffff, #0a0d1a); + --color-site-secondary: light-dark(#ffffff, #151a30); + --color-site-secondary-hover: light-dark(#f4f4f6, #1c2240); + --color-site-secondary-border: light-dark(#00000024, #f4f5f733); + --color-site-clearing: light-dark(oklab(98.8% 0.0004 -0.001), oklab(14.2% 0.001 -0.02)); + --color-site-dusk: light-dark(oklab(98.8% 0.0004 -0.001), oklab(16.3% 0.001 -0.028)); +} + +/* Images can't be light-dark() pairs, so each scheme sets them (as @polaris/ui's assets.css does). */ +:root { + --site-star: url("../../../design/assets/icons/px-polaris-dark.svg"); + --site-hand: url("../../../design/assets/icons/px-hand-needs-dark.svg"); + --site-constellation: url("../../../design/assets/icons/px-constellation-dark.svg"); + --site-dither-claude: url("../../../design/assets/dither/dither-claude-16.svg"); + --site-dither-codex: url("../../../design/assets/dither/dither-codex-16.svg"); + --site-logo: url("../../../design/assets/brand/polaris-logo-starlight.svg"); + --site-halo-display: block; + --site-shot-shadow: 0 0 0 1px #ffffff1f, 0 40px 100px #00000099; + --site-hero-shadow: 0 0 0 1px #ffffff1f, 0 40px 120px #000000b3, 0 0 0 10px #bcd3ff0a; + --site-card-shadow: 0 40px 100px #00000080; + --site-primary-shadow: 0 0 0 1px #ffffff, 0 10px 30px #00000066; +} + +@media (prefers-color-scheme: light) { + :root { + --site-star: url("../../../design/assets/icons/px-polaris-light.svg"); + --site-hand: url("../../../design/assets/icons/px-hand-needs-light.svg"); + --site-constellation: url("../../../design/assets/icons/px-constellation-light.svg"); + --site-dither-claude: url("../../../design/assets/dither/dither-claude-16-light.svg"); + --site-dither-codex: url("../../../design/assets/dither/dither-codex-16-light.svg"); + --site-logo: url("../../../design/assets/brand/polaris-logo-blue.svg"); + --site-halo-display: none; + --site-shot-shadow: 0 0 0 1px #0000001a, 0 30px 80px #1f2a4a2e; + --site-hero-shadow: 0 0 0 1px #0000001a, 0 40px 100px #1f2a4a40; + --site-card-shadow: 0 30px 80px #1f2a4a1f; + --site-primary-shadow: 0 8px 24px #17181a33; + } +} + +@layer base { + html { + background-color: var(--color-site-ground); + scroll-behavior: smooth; + } + + body { + background-color: var(--color-site-ground); + color: var(--color-site-copy); + font-size: 16px; + line-height: 24px; + } + + @media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + } +} + +/* The hero sky: the night scene at 4/3 (S1), or the dawn scene at 1× fading into bg-light (S2). */ +@utility hero-sky { + background-color: var(--color-site-ground); + background-image: var(--scene); + background-repeat: no-repeat; + background-size: max(1920px, 133.334vw) auto; + background-position: center -20px; + image-rendering: pixelated; + + @media (width < 64rem) { + background-position: calc(50% - 295px) 20px; + } + + @media (prefers-color-scheme: light) { + background-image: + linear-gradient( + to bottom, + transparent calc(max(900px, 62.5vw) - 160px), + var(--color-site-ground) calc(max(900px, 62.5vw) + 10px) + ), + var(--scene); + background-size: + 100% 100%, + max(1440px, 100vw) auto; + background-position: + 0 0, + center top; + } +} + +/* The closing sky: the scene's lower half under a fade from the page above. */ +@utility cta-sky { + background-color: var(--color-site-ground); + background-image: + linear-gradient(to bottom, var(--color-site-dusk) 0%, transparent 30%), var(--scene); + background-repeat: no-repeat; + background-size: + 100% 100%, + max(1920px, 133.334vw) auto; + background-position: + 0 0, + left -200px; + image-rendering: pixelated; + + @media (width < 64rem) { + background-position: + 0 0, + calc(50% + 325px) -330px; + } + + @media (prefers-color-scheme: light) { + background-image: + linear-gradient(to bottom, var(--color-site-ground) 0%, transparent 30%), var(--scene); + background-size: + 100% 100%, + max(1440px, 100vw) auto; + background-position: + 0 0, + center bottom; + } +} + +/* A soft clearing behind text on a scene (rule/scene-text-contrast). */ +@utility clearing { + background-image: radial-gradient( + ellipse 55% 55% at 50% 50% in oklab, + color-mix(in oklab, var(--color-site-clearing) 95%, transparent) 0%, + color-mix(in oklab, var(--color-site-clearing) 81.6%, transparent) 60%, + transparent 100% + ); + + @media (width < 64rem) { + background-image: radial-gradient( + ellipse 95% 60% at 50% 55% in oklab, + color-mix(in oklab, var(--color-site-clearing) 95%, transparent) 0%, + color-mix(in oklab, var(--color-site-clearing) 84.7%, transparent) 65%, + transparent 100% + ); + } +} + +/* Pixel art and washes by name; a var() url in an inline style would resolve against the page. */ +@utility px-star { + background-image: var(--site-star); +} + +@utility px-hand { + background-image: var(--site-hand); +} + +@utility px-constellation { + background-image: var(--site-constellation); +} + +@utility px-dither-claude { + background-image: var(--site-dither-claude); +} + +@utility px-dither-codex { + background-image: var(--site-dither-codex); +} + +@utility px-logo { + background-image: var(--site-logo); +} + +@utility wash-claude-code { + background-image: var(--wash-claude-code); +} + +@utility wash-codex { + background-image: var(--wash-codex); +} + +@utility halo-starlight { + display: var(--site-halo-display); + background-image: var(--halo-starlight); +} + +@utility px-icon { + display: inline-block; + flex-shrink: 0; + background-repeat: no-repeat; + background-size: 100% 100%; + image-rendering: pixelated; +} + +/* A product shot cropped like its Paper frame: --w/--x/--y on desktop, --mw/--mx/--my on mobile. */ +@utility shot-frame { + position: relative; + overflow: hidden; + aspect-ratio: var(--mar, 370 / 300); + box-shadow: var(--site-shot-shadow); + + @media (width >= 64rem) { + aspect-ratio: var(--ar, 804 / 620); + } +} + +@utility shot-image { + position: absolute; + max-width: none; + height: auto; + width: var(--mw); + left: var(--mx, 0%); + top: var(--my, 0%); + + @media (width >= 64rem) { + width: var(--w); + left: var(--x, 0%); + top: var(--y, 0%); + } + + @media (prefers-color-scheme: light) { + left: var(--lmx, var(--mx, 0%)); + top: var(--lmy, var(--my, 0%)); + + @media (width >= 64rem) { + left: var(--lx, var(--x, 0%)); + top: var(--ly, var(--y, 0%)); + } + } +} diff --git a/apps/site/app/layout.tsx b/apps/site/app/layout.tsx new file mode 100644 index 00000000..f543169f --- /dev/null +++ b/apps/site/app/layout.tsx @@ -0,0 +1,40 @@ +import type { Metadata, Viewport } from "next"; +import type { ReactNode } from "react"; +import appleIcon from "../../../design/assets/app-icon/polaris.iconset/icon_256x256.png"; +import icon from "../../../design/assets/app-icon/polaris.iconset/icon_32x32@2x.png"; +import shot from "../../../design/assets/site/shot-orchestrate.png"; +import { site } from "../components/links"; +import "./globals.css"; + +export const metadata: Metadata = { + metadataBase: new URL(site.origin), + title: `${site.name}: ${site.tagline}`, + description: site.description, + icons: { icon: icon.src, apple: appleIcon.src }, + openGraph: { + type: "website", + siteName: site.name, + title: site.tagline, + description: site.description, + images: [ + { url: shot.src, width: shot.width, height: shot.height, alt: "The Polaris Orchestrator" }, + ], + }, + twitter: { card: "summary_large_image" }, +}; + +export const viewport: Viewport = { + colorScheme: "dark light", + themeColor: [ + { media: "(prefers-color-scheme: dark)", color: "#0a0d1a" }, + { media: "(prefers-color-scheme: light)", color: "#fbfbfc" }, + ], +}; + +export default function RootLayout({ children }: { readonly children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/site/app/page.tsx b/apps/site/app/page.tsx new file mode 100644 index 00000000..b94d0dbb --- /dev/null +++ b/apps/site/app/page.tsx @@ -0,0 +1,25 @@ +import { Features } from "../components/features"; +import { FinalCta } from "../components/final-cta"; +import { Footer } from "../components/footer"; +import { HarnessStrip } from "../components/harness-strip"; +import { Hero } from "../components/hero"; +import { Hosts } from "../components/hosts"; +import { Nav } from "../components/nav"; +import { OpenSource } from "../components/open-source"; + +export default function Home() { + return ( +
+
+ ); +} diff --git a/apps/site/components/actions.tsx b/apps/site/components/actions.tsx new file mode 100644 index 00000000..c2f991bf --- /dev/null +++ b/apps/site/components/actions.tsx @@ -0,0 +1,77 @@ +import { EmailDownload } from "./email-download"; +import { AppleIcon, GitHubIcon } from "./icons"; +import { links } from "./links"; + +const base = + "inline-flex shrink-0 items-center justify-center gap-2.5 rounded-[10px] font-medium transition-[background-color,border-color,transform] duration-150 active:scale-[0.98] motion-reduce:transition-none motion-reduce:active:scale-100"; + +const primary = `${base} bg-site-primary text-site-on-primary shadow-(--site-primary-shadow) hover:bg-site-primary-hover`; + +const secondary = `${base} border border-site-secondary-border bg-site-secondary text-text-strong hover:bg-site-secondary-hover`; + +const large = "h-12 pl-[18px] pr-[22px] text-[16px] leading-5"; + +/** The one primary action: the Mac download (a route that redirects to the latest DMG). */ +export function DownloadButton() { + return ( + + + Download for macOS + + ); +} + +/** The secondary action is always the source. */ +const sourceSizes = { + large: `${large} pr-5`, + block: "h-[50px] w-full text-[16px] leading-5", + compact: "h-11 rounded-[8px] px-[18px] text-[15px] leading-5", +} as const; + +export function SourceButton({ size = "large" }: { readonly size?: keyof typeof sourceSizes }) { + return ( + + + View source + + ); +} + +export function NavDownload() { + return ( + + Download + + ); +} + +/** Desktop: Download and source side by side. Phones: email the Mac download instead (S3). */ +export function Actions({ + note, + mobileNote, +}: { + readonly note?: string | undefined; + readonly mobileNote?: string | undefined; +}) { + return ( + <> +
+
+ + +
+ {note === undefined ? null :

{note}

} +
+
+ + + {mobileNote === undefined ? null : ( +

{mobileNote}

+ )} +
+ + ); +} diff --git a/apps/site/components/email-download.test.ts b/apps/site/components/email-download.test.ts new file mode 100644 index 00000000..a07f4588 --- /dev/null +++ b/apps/site/components/email-download.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; +import { mailtoDownload } from "./email-download"; +import { links } from "./links"; + +describe("mailtoDownload", () => { + test("drafts the download link to the given address", () => { + const url = new URL(mailtoDownload("ada@example.com")); + + expect(url.protocol).toBe("mailto:"); + expect(url.pathname).toBe("ada%40example.com"); + expect(url.searchParams.get("subject")).toBe("Polaris for Mac"); + expect(url.searchParams.get("body")).toBe( + "Download Polaris for macOS: https://polaris.lux.dev/download/mac" + ); + }); + + test("encodes an address that would otherwise break the query", () => { + const url = mailtoDownload("a+b&subject=x@example.com"); + + expect(url.startsWith("mailto:a%2Bb%26subject%3Dx%40example.com?")).toBe(true); + }); + + test("spaces are percent-encoded, not plus signs, for mail clients", () => { + expect(mailtoDownload("a@example.com")).not.toContain("+"); + }); +}); + +describe("links", () => { + test("the primary action goes to the download route", () => { + expect(links.download).toBe("/download/mac"); + }); +}); diff --git a/apps/site/components/email-download.tsx b/apps/site/components/email-download.tsx new file mode 100644 index 00000000..de2cd44b --- /dev/null +++ b/apps/site/components/email-download.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useId, useState, type FormEvent } from "react"; +import { links, site } from "./links"; + +/** A draft to the visitor's own address carrying the Mac download link. */ +export function mailtoDownload(email: string): string { + const body = `Download Polaris for macOS: ${site.origin}${links.download}`; + + const query = new URLSearchParams({ subject: "Polaris for Mac", body }); + + return `mailto:${encodeURIComponent(email)}?${query.toString().replaceAll("+", "%20")}`; +} + +/** + * A phone can't install a Mac app, so it drafts the download link to the visitor's own address + * in their mail app. Nothing is sent to or stored by Polaris. + */ +export function EmailDownload() { + const id = useId(); + + const [email, setEmail] = useState(""); + + const send = (event: FormEvent) => { + event.preventDefault(); + + window.location.href = mailtoDownload(email); + }; + + return ( +
+ + setEmail(event.target.value)} + required + autoComplete="email" + placeholder="you@example.com" + className="border-site-field-border bg-site-field text-text-strong placeholder:text-site-kicker h-[50px] w-full rounded-[10px] border px-4 text-[16px] leading-5" + /> + +
+ ); +} diff --git a/apps/site/components/features.tsx b/apps/site/components/features.tsx new file mode 100644 index 00000000..f32ce239 --- /dev/null +++ b/apps/site/components/features.tsx @@ -0,0 +1,153 @@ +import constellation from "../../../design/assets/site/shot-constellation.png"; +import needsYou from "../../../design/assets/site/shot-needs-you-light.png"; +import reviewLight from "../../../design/assets/site/shot-review-light.png"; +import review from "../../../design/assets/site/shot-review.png"; +import { Pixel, type PixelArt } from "./pixel"; +import { container, display, lead, SectionHeader } from "./section-header"; +import { Shot, type ShotProps } from "./shot"; + +interface Feature { + readonly kicker: string; + readonly icon: PixelArt; + readonly title: string; + readonly body: string; + readonly mobileBody?: string; + readonly facts: readonly [string, string, string]; + readonly shot: Omit; + /** Which side the shot bleeds off on desktop; on phones every shot bleeds right. */ + readonly bleed: "left" | "right"; +} + +const features: readonly Feature[] = [ + { + kicker: "Needs you", + icon: "hand", + title: "Only the stuck agent asks for you.", + body: "When an agent wants to run a command or needs a decision, it goes to the top of one inbox. Approve it, answer it or take over in the terminal. The rest keep working.", + facts: [ + "One inbox for every agent", + "Answer straight from the notification", + "Take over in the terminal, then hand it back", + ], + shot: { + alt: "An agent session that needs you: a command waiting for approval at the top of the session list.", + dark: needsYou, + desktop: { w: 174.13 }, + mobile: { w: 272.43, y: -13.33 }, + }, + bleed: "right", + }, + { + kicker: "Review", + icon: "star", + title: "Read the risky lines first.", + body: "A reviewer reads every change and ranks what it finds. Mark a finding as fine and it learns, for one change, one repo or everywhere. Critical findings always show.", + facts: [ + "A risk summary for every turn and pull request", + "Review in its own checkout, not your working tree", + "Gets sharper with every verdict", + ], + shot: { + alt: "A review with ranked findings beside the diff they point at.", + dark: review, + light: reviewLight, + desktop: { w: 205.97, x: -39.3, y: -19.35 }, + mobile: { w: 272.43, x: -51.08, y: -28 }, + lightDesktop: { w: 205.97, x: -37.81, y: -8.23 }, + lightMobile: { w: 272.43, x: -50, y: -10.3 }, + }, + bleed: "left", + }, + { + kicker: "Constellation", + icon: "constellation", + title: "One plan. Many agents.", + body: "Hand a large change to one agent. It splits the work into tasks, starts an agent on each and holds every merge for review. You see the whole plan as a graph and can steer any agent in it.", + mobileBody: + "Hand a large change to one agent. It splits the work into tasks, starts an agent on each and holds every merge for review. Steer any agent in the graph.", + facts: [ + "Claude Code and Codex in the same plan", + "Every task on its own worktree", + "Nothing merges without a review", + ], + shot: { + alt: "A constellation: one plan split into tasks, each with its own agent, shown as a graph.", + dark: constellation, + desktop: { w: 197.01, x: -97.39, y: -14.84 }, + mobile: { w: 272.43, x: -134.59, y: -19.67 }, + }, + bleed: "right", + }, +]; + +const gutterLeft = "lg:pl-16 xl:pl-[max(120px,calc((100%-1200px)/2))]"; + +const gutterRight = "lg:pr-16 xl:pr-[max(120px,calc((100%-1200px)/2))]"; + +export function Features() { + return ( +
+
+ +
+

+ Supervise your agents. Stop babysitting them. +

+

+ Agents work for hours, in parallel. Polaris stays quiet until one needs a decision, a + review or a hand. +

+
+
+ {features.map((feature, index) => ( + + ))} +
+ ); +} + +function FeatureRow({ feature, first }: { readonly feature: Feature; readonly first: boolean }) { + const shotLeft = feature.bleed === "left"; + + const direction = shotLeft ? "flex-col lg:flex-row" : "flex-col lg:flex-row-reverse"; + + const gutter = shotLeft ? gutterRight : gutterLeft; + + const corners = shotLeft + ? "rounded-l-[12px] lg:rounded-l-none lg:rounded-r-card" + : "rounded-l-[12px] lg:rounded-l-card"; + + return ( +
+ +
+

+ + {feature.kicker} +

+

+ {feature.title} +

+

+ + {feature.body} + + {feature.mobileBody === undefined ? null : ( + {feature.mobileBody} + )} +

+
    + {feature.facts.map((fact) => ( +
  • — {fact}
  • + ))} +
+
+
+ ); +} diff --git a/apps/site/components/final-cta.tsx b/apps/site/components/final-cta.tsx new file mode 100644 index 00000000..15940076 --- /dev/null +++ b/apps/site/components/final-cta.tsx @@ -0,0 +1,30 @@ +import { Actions } from "./actions"; +import { Pixel } from "./pixel"; + +/** The closing sky: one star, one line, the same two actions as the hero. */ +export function FinalCta() { + return ( +
+
+ + +

+ Steer every agent by one star. +

+

+ Free for macOS. Open source under Apache-2.0. + Free and open source, for macOS. +

+
+ +
+
+
+ ); +} diff --git a/apps/site/components/footer.tsx b/apps/site/components/footer.tsx new file mode 100644 index 00000000..eae890dd --- /dev/null +++ b/apps/site/components/footer.tsx @@ -0,0 +1,86 @@ +import { links } from "./links"; +import { Lockup } from "./nav"; + +const columns = [ + { + title: "Product", + items: [ + { label: "Download", href: links.download }, + { label: "Changelog", href: links.changelog }, + { label: "Docs", href: links.docs }, + ], + }, + { + title: "Open source", + items: [ + { label: "GitHub", href: links.source }, + { label: "License", href: links.license }, + { label: "Attribution", href: links.attribution }, + ], + }, + { + title: "Brand", + items: [ + { label: "Brand guidelines", short: "Guidelines", href: links.brand }, + { label: "Press kit", href: links.pressKit }, + ], + }, +] as const; + +export function Footer() { + return ( + + ); +} diff --git a/apps/site/components/harness-strip.tsx b/apps/site/components/harness-strip.tsx new file mode 100644 index 00000000..d76b3af9 --- /dev/null +++ b/apps/site/components/harness-strip.tsx @@ -0,0 +1,41 @@ +export function HarnessStrip() { + return ( +
+

+ Works with the agents you already use +

+
+ ); +} + +function Harness({ + name, + wash, + dither, +}: { + readonly name: string; + readonly wash: string; + readonly dither: string; +}) { + return ( +
  • +
  • + ); +} diff --git a/apps/site/components/hero.tsx b/apps/site/components/hero.tsx new file mode 100644 index 00000000..a3d622d3 --- /dev/null +++ b/apps/site/components/hero.tsx @@ -0,0 +1,49 @@ +import orchestrateLight from "../../../design/assets/site/shot-orchestrate-light.png"; +import orchestrate from "../../../design/assets/site/shot-orchestrate.png"; +import { Actions } from "./actions"; +import { Shot } from "./shot"; + +export function Hero() { + return ( +
    +
    +

    +

    +

    + The north star for your agents. +

    +

    + Run Claude Code and Codex side by side, on any machine. Polaris shows what every agent is + doing, calls you only when one is stuck, and puts the riskiest changes first. +

    +
    + +
    +
    +
    + +
    +
    + ); +} diff --git a/apps/site/components/hosts.tsx b/apps/site/components/hosts.tsx new file mode 100644 index 00000000..deacaa23 --- /dev/null +++ b/apps/site/components/hosts.tsx @@ -0,0 +1,144 @@ +import { LaptopIcon, ServerIcon } from "./icons"; +import { container, display, lead, SectionHeader } from "./section-header"; + +interface Host { + readonly name: string; + readonly detail: string; + readonly local: boolean; + /** The Harnesses working there, as dither tiles. */ + readonly agents: readonly ("claude" | "codex")[]; + readonly working: string; +} + +const hosts: readonly Host[] = [ + { + name: "This Mac", + detail: "local · 5 workspaces", + local: true, + agents: ["claude", "claude", "codex"], + working: "3 working", + }, + { + name: "build-01", + detail: "ssh · 4 ms · 3 workspaces", + local: false, + agents: ["codex"], + working: "1 working", + }, + { + name: "dev-vm", + detail: "ssh · 21 ms · 2 workspaces", + local: false, + agents: ["codex", "claude"], + working: "2 working", + }, + { + name: "staging", + detail: "ssh · 38 ms · 1 workspace", + local: false, + agents: ["codex"], + working: "1 working", + }, +]; + +const facts = [ + { + title: "Uses the SSH you already have", + body: "Pick a host from your SSH config. Polaris shows what it will install and asks first.", + }, + { + title: "Stays out of the way", + body: "The daemon idles quietly, so it's at home on a small VM as much as a workstation.", + }, + { + title: "Picks up where you left off", + body: "Lose the connection and Polaris reconnects, then catches you up on every turn you missed.", + }, +] as const; + +export function Hosts() { + return ( +
    + +
    +
    +

    + Wherever your code lives. +

    +

    + Run agents on this Mac or on any machine you can reach over SSH. Every session shows up + in the same window, and remote agents keep working after you close the lid. +

    +
    + +
    +
      + {facts.map((fact) => ( +
    • +

      + {fact.title} +

      +

      + {fact.body} +

      +
    • + ))} +
    +
    + ); +} + +/** An illustration of the Hosts list, drawn rather than shot; it is not interactive. */ +function HostPanel() { + return ( +
    +
    + Hosts + + 4 hosts · 11 workspaces · 7 working + +
    + +
    + ); +} + +function HostRow({ host }: { readonly host: Host }) { + return ( +
  • + + {host.local ? : } + + + {host.name} + {host.detail} + + + {host.agents.map((agent, index) => ( + + ))} + + + {host.working} + +
  • + ); +} diff --git a/apps/site/components/icons.tsx b/apps/site/components/icons.tsx new file mode 100644 index 00000000..a91ed16f --- /dev/null +++ b/apps/site/components/icons.tsx @@ -0,0 +1,88 @@ +/** Inline glyphs from the Paper artboards; decorative unless a caller labels them. */ +const GITHUB_PATH = + "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"; + +const APPLE_PATH = + "M788 341c-6 4-108 62-108 190 0 148 130 200 134 202-1 3-21 72-69 142-43 62-88 124-156 124s-86-40-165-40c-77 0-104 41-166 41s-106-58-156-128C44 790 0 669 0 553c0-186 121-285 240-285 63 0 116 42 156 42 38 0 97-44 169-44 27 0 125 2 190 95zM554 167c30-35 51-84 51-133 0-7-1-14-2-19-48 2-106 32-140 72-27 31-53 80-53 130 0 8 1 15 2 18 3 1 8 1 13 1 43 0 97-29 129-69z"; + +export function GitHubIcon({ size = 16 }: { readonly size?: number }) { + return ( + + ); +} + +export function AppleIcon() { + return ( + + ); +} + +export function LaptopIcon() { + return ( + + ); +} + +export function ServerIcon() { + return ( + + ); +} + +export function MenuIcon() { + return ( + + ); +} diff --git a/apps/site/components/links.ts b/apps/site/components/links.ts new file mode 100644 index 00000000..13f756fe --- /dev/null +++ b/apps/site/components/links.ts @@ -0,0 +1,28 @@ +/** Where the site's links go. The download route redirects to the latest Release's DMG. */ +export const repo = "https://github.com/luxdotdev/polaris"; + +export const links = { + download: "/download/mac", + source: repo, + docs: `${repo}#readme`, + changelog: `${repo}/releases`, + license: `${repo}/blob/main/LICENSE`, + attribution: `${repo}/blob/main/ATTRIBUTION.md`, + brand: `${repo}/blob/main/DESIGN.md#brand`, + pressKit: `${repo}/tree/main/design/assets/brand`, +} as const; + +export const navLinks = [ + { label: "Features", href: "#features" }, + { label: "Hosts", href: "#hosts" }, + { label: "Docs", href: links.docs }, + { label: "Changelog", href: links.changelog }, +] as const; + +export const site = { + name: "Polaris", + origin: "https://polaris.lux.dev", + tagline: "The north star for your agents.", + description: + "Run Claude Code and Codex side by side, on any machine. Polaris shows what every agent is doing, calls you only when one is stuck, and puts the riskiest changes first.", +} as const; diff --git a/apps/site/components/mobile-menu.tsx b/apps/site/components/mobile-menu.tsx new file mode 100644 index 00000000..dc15dcd5 --- /dev/null +++ b/apps/site/components/mobile-menu.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useId } from "react"; +import { GitHubIcon, MenuIcon } from "./icons"; +import { links, navLinks } from "./links"; + +const item = "flex h-12 items-center gap-2.5 text-[17px] leading-6 text-text-strong"; + +/** The phone menu: a native popover under the bar, dismissed by a tap outside or a choice. */ +export function MobileMenu() { + const id = useId(); + + const close = () => document.getElementById(id)?.hidePopover(); + + return ( + <> + +
    + +
    + + ); +} diff --git a/apps/site/components/nav.tsx b/apps/site/components/nav.tsx new file mode 100644 index 00000000..a6c96158 --- /dev/null +++ b/apps/site/components/nav.tsx @@ -0,0 +1,60 @@ +import { NavDownload } from "./actions"; +import { GitHubIcon } from "./icons"; +import { links, navLinks } from "./links"; +import { MobileMenu } from "./mobile-menu"; +import { Pixel } from "./pixel"; + +const link = "text-[14px] leading-5 text-site-link transition-colors hover:text-text-strong"; + +export function Lockup({ size }: { readonly size: 18 | 20 }) { + return ( + + + + Polaris + + + ); +} + +/** A solid bar, never text straight on the sky (rule/scene-text-contrast). */ +export function Nav() { + return ( +
    + +
    + ); +} diff --git a/apps/site/components/open-source.tsx b/apps/site/components/open-source.tsx new file mode 100644 index 00000000..08e6aa47 --- /dev/null +++ b/apps/site/components/open-source.tsx @@ -0,0 +1,96 @@ +import { SourceButton } from "./actions"; +import { GitHubIcon } from "./icons"; +import { links } from "./links"; +import { Pixel } from "./pixel"; +import { container, display, lead, SectionHeader } from "./section-header"; + +const promises = [ + { + title: "Apache-2.0", + body: "Read it, fork it, build on it. That covers the app and the daemon.", + }, + { title: "No sign-in", body: "Download it and it opens. No account, no seats, no trial." }, + { + title: "Your agents", + body: "Polaris drives your own Claude Code and Codex, on the plans you already have.", + }, + { + title: "Your machines", + body: "The app connects straight to your hosts over SSH. No Polaris server sits in between.", + }, +] as const; + +export function OpenSource() { + return ( +
    + +

    + Free. Open source. No account. +

    +

    + No subscription, no trial, no sign-in. Polaris is Apache-2.0: download it, run it, read + every line. +

    +
    +
      + {promises.map((promise) => ( +
    • +

      + {promise.title} +

      +

      + {promise.body} +

      +
    • + ))} +
    + +
    +
    + ); +} + +/** The repository, open: where the notify form stood before the source went public. */ +function RepoCard() { + return ( +
    +
    + ); +} diff --git a/apps/site/components/pixel.tsx b/apps/site/components/pixel.tsx new file mode 100644 index 00000000..ab740de2 --- /dev/null +++ b/apps/site/components/pixel.tsx @@ -0,0 +1,36 @@ +/** Pixel art from design/assets, themed through the CSS variables in app/globals.css. */ +export type PixelArt = + | "star" + | "hand" + | "constellation" + | "dither-claude" + | "dither-codex" + | "logo"; + +// Whole class names, so Tailwind's scanner emits each utility. +const artClass = { + star: "px-star", + hand: "px-hand", + constellation: "px-constellation", + "dither-claude": "px-dither-claude", + "dither-codex": "px-dither-codex", + logo: "px-logo", +} satisfies Record; + +export function Pixel({ + art, + size, + className = "", +}: { + readonly art: PixelArt; + readonly size: number; + readonly className?: string; +}) { + return ( + ); diff --git a/apps/desktop/src/renderer/features/machines/updates/UpgradeStatus.tsx b/apps/desktop/src/renderer/features/machines/updates/UpgradeStatus.tsx new file mode 100644 index 00000000..ab6e3cd5 --- /dev/null +++ b/apps/desktop/src/renderer/features/machines/updates/UpgradeStatus.tsx @@ -0,0 +1,99 @@ +/** Machines already carry Upgrade outcomes; captions expire once, without idle polling. */ +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@polaris/ui"; +import { type ReactNode, useEffect, useState } from "react"; +import { useStore } from "zustand"; +import { createStore } from "zustand/vanilla"; +import type { MachineView } from "../../../../shared/api.ts"; +import { useNav } from "../../../shell/hooks.ts"; +import { useMachines } from "../hooks.tsx"; +import { UPGRADE_CAPTION_MS, upgradeCaption, upgradeHover } from "./quiet.ts"; + +interface UpgradeStatusState { + readonly machines: ReadonlyArray; + readonly opened: Readonly>; + readonly now: number; +} + +const statusStore = createStore(() => ({ + machines: [], + opened: {}, + now: Date.now(), +})); + +export const acknowledgeUpgrade = (hostKey: string) => + statusStore.setState((state) => ({ opened: { ...state.opened, [hostKey]: Date.now() } })); + +export const UpgradeStatusPublisher = () => { + const machines = useMachines(); + const hostKey = useNav((s) => s.hostKey); + useEffect(() => { + if (hostKey !== null) acknowledgeUpgrade(hostKey); + }, [hostKey]); + useEffect(() => { + const now = Date.now(); + statusStore.setState({ machines: machines ?? [], now }); + + const expirations = (machines ?? []).flatMap((m) => { + const last = m.daemon?.lastUpdate; + + return last?.result === "updated" + ? [last.at + UPGRADE_CAPTION_MS, last.at + 24 * UPGRADE_CAPTION_MS].filter((at) => at > now) + : []; + }); + + if (expirations.length === 0) return undefined; + + const timers = [...new Set(expirations)].map((at) => + setTimeout(() => statusStore.setState({ now: Date.now() }), at - now) + ); + + return () => timers.forEach(clearTimeout); + }, [machines]); + + return null; +}; + +export const useUpgradeClock = () => useStore(statusStore, (s) => s.now); + +export const useUpgradeCaption = (hostKey: string): string | null => + useStore(statusStore, (s) => + upgradeCaption( + s.machines.find((m) => m.key === hostKey)?.daemon?.lastUpdate ?? null, + s.opened[hostKey], + s.now + ) + ); + +export const UpgradeHover = ({ + hostKey, + children, +}: { + readonly hostKey: string; + readonly children: ReactNode; +}) => { + const last = useStore( + statusStore, + (s) => s.machines.find((m) => m.key === hostKey)?.daemon?.lastUpdate ?? null + ); + + const [now, setNow] = useState(() => Date.now()); + const note = upgradeHover(last, now); + + if (note === null) return children; + + return ( + { + if (open) setNow(Date.now()); + }} + > + {children} + +
    +

    {note.title}

    +

    {note.body}

    +
    +
    +
    + ); +}; diff --git a/apps/desktop/src/renderer/features/machines/updates/model.test.ts b/apps/desktop/src/renderer/features/machines/updates/model.test.ts index e1fea295..b2cf73bc 100644 --- a/apps/desktop/src/renderer/features/machines/updates/model.test.ts +++ b/apps/desktop/src/renderer/features/machines/updates/model.test.ts @@ -43,15 +43,19 @@ describe("a host's daemon update line", () => { }); test("offers the update with both versions, and waits for a disconnected host", () => { - expect(line(daemon({ updateAvailable: true }))).toEqual({ + expect(line(daemon({ updateAvailable: true, keepUpToDate: false }))).toEqual({ kind: "available", - text: "Update available · 0.4.1 → 0.5.0", + text: "0.5.0 available · upgrades its daemon only when you ask", canUpdate: true, caption: null, }); - expect(line(daemon({ updateAvailable: true }), false)).toMatchObject({ + expect(line(daemon({ updateAvailable: true, keepUpToDate: false }), false)).toMatchObject({ canUpdate: false, - caption: "Updates once pi is connected", + caption: "Upgrades once pi is connected", + }); + expect(line(daemon({ updateAvailable: true }), false)).toEqual({ + kind: "note", + text: "Upgrades to 0.5.0 when it connects", }); }); @@ -92,7 +96,7 @@ describe("a host's daemon update line", () => { }) ); - expect(done(3 * 60_000)).toEqual({ kind: "updated", text: "Updated to 0.5.0 · 3m ago" }); + expect(done(3 * 60_000)).toEqual({ kind: "updated", text: "Upgraded to 0.5.0 · 3m ago" }); expect(done(25 * 60 * 60_000).kind).toBe("none"); }); @@ -142,7 +146,7 @@ describe("a host's daemon update line", () => { failed(problem({ kind: "unsupported", message: "FreeBSD isn't supported." })) ).toMatchObject({ failure: { reason: "unsupported", actions: [] } }); expect(failed(null)).toMatchObject({ - failure: { reason: "failed", title: "Updating pi didn't finish" }, + failure: { reason: "failed", title: "Upgrading pi didn't finish" }, }); }); }); @@ -154,7 +158,7 @@ describe("the per-host override", () => { expect(overrideChoice(daemon({ keepUpToDateOverride: false }))).toBe("off"); expect(line(daemon({ keepUpToDateOverride: false }))).toEqual({ kind: "note", - text: "Updates its daemon only when you ask", + text: "Upgrades its daemon only when you ask", }); }); }); diff --git a/apps/desktop/src/renderer/features/machines/updates/model.ts b/apps/desktop/src/renderer/features/machines/updates/model.ts index 3e3ee7bc..894973a4 100644 --- a/apps/desktop/src/renderer/features/machines/updates/model.ts +++ b/apps/desktop/src/renderer/features/machines/updates/model.ts @@ -5,7 +5,7 @@ */ import type { DaemonUpdateProblem, DaemonUpdateView } from "./contract.ts"; -/** How long "Updated to …" stays on the row after an update. */ +/** How long "Upgraded to …" stays on the row after an update. */ export const UPDATED_NOTE_MS = 24 * 60 * 60 * 1000; export type FailureAction = "retry" | "copy-command" | "open-ssh"; @@ -141,7 +141,7 @@ export const updateFailure = ( default: return { reason: "failed", - title: `Updating ${label} didn't finish`, + title: `Upgrading ${label} didn't finish`, body: `${kept} Nothing half-installed is left running.`, detail, actions: [RETRY], @@ -209,20 +209,23 @@ export const updateLine = ({ }; if (daemon.updateAvailable) { - const from = daemon.installedVersion ?? "?"; + const target = daemon.bundledVersion ?? "?"; + + if (!connected && daemon.keepUpToDate) + return { kind: "note", text: `Upgrades to ${target} when it connects` }; return { kind: "available", - text: `Update available · ${from} → ${daemon.bundledVersion ?? "?"}`, + text: `${target} available${daemon.keepUpToDate ? "" : " · upgrades its daemon only when you ask"}`, canUpdate: connected, - caption: connected ? null : `Updates once ${label} is connected`, + caption: connected ? null : `Upgrades once ${label} is connected`, }; } if (last?.result === "updated" && now - last.at < UPDATED_NOTE_MS) return { kind: "updated", - text: `Updated to ${last.version ?? daemon.installedVersion ?? "?"} · ${ago(now - last.at)}`, + text: `Upgraded to ${last.version ?? daemon.installedVersion ?? "?"} · ${ago(now - last.at)}`, }; if (daemon.keepUpToDateOverride === null) return NONE; @@ -231,7 +234,7 @@ export const updateLine = ({ kind: "note", text: daemon.keepUpToDateOverride ? "Keeps its daemon up to date on its own" - : "Updates its daemon only when you ask", + : "Upgrades its daemon only when you ask", }; }; diff --git a/apps/desktop/src/renderer/features/machines/updates/quiet.test.ts b/apps/desktop/src/renderer/features/machines/updates/quiet.test.ts new file mode 100644 index 00000000..574fd7f9 --- /dev/null +++ b/apps/desktop/src/renderer/features/machines/updates/quiet.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "bun:test"; +import type { DaemonUpdateResult } from "../../../../shared/daemonUpdates.ts"; +import { UPGRADE_CAPTION_MS, upgradeCaption, upgradeHover } from "./quiet.ts"; + +const NOW = 1_800_000_000_000; + +const last: DaemonUpdateResult = { + at: NOW, + result: "updated", + from: "0.4.0", + version: "0.5.0", + problem: null, +}; + +describe("quiet Daemon Upgrade status", () => { + test("caption expires after exactly an hour or when the host opens", () => { + expect(upgradeCaption(last, undefined, NOW + UPGRADE_CAPTION_MS - 1)).toBe("Upgraded to 0.5.0"); + expect(upgradeCaption(last, undefined, NOW + UPGRADE_CAPTION_MS)).toBeNull(); + expect(upgradeCaption(last, NOW, NOW)).toBeNull(); + expect(upgradeCaption(last, NOW - 1, NOW)).toBe("Upgraded to 0.5.0"); + }); + test("opening an old upgrade doesn't suppress a later upgrade", () => { + expect(upgradeCaption({ ...last, at: NOW + 1000 }, NOW, NOW + 1000)).toBe("Upgraded to 0.5.0"); + }); + test("the hover states both versions and that sessions kept working, for a day", () => { + expect(upgradeHover(last, NOW + 180_000)).toEqual({ + title: "Daemon upgraded to 0.5.0 · 3m ago", + body: "From 0.4.0, to match this Mac. Agent sessions kept working.", + }); + expect(upgradeHover(last, NOW + 24 * UPGRADE_CAPTION_MS)).toBeNull(); + }); + test("no success status for failed, current, or missing outcomes", () => { + expect(upgradeHover(null, NOW)).toBeNull(); + + for (const result of ["current", "newer", "failed"] as const) { + expect(upgradeCaption({ ...last, result }, undefined, NOW)).toBeNull(); + expect(upgradeHover({ ...last, result }, NOW)).toBeNull(); + } + }); +}); diff --git a/apps/desktop/src/renderer/features/machines/updates/quiet.ts b/apps/desktop/src/renderer/features/machines/updates/quiet.ts new file mode 100644 index 00000000..6322a218 --- /dev/null +++ b/apps/desktop/src/renderer/features/machines/updates/quiet.ts @@ -0,0 +1,45 @@ +/** Quiet Upgrade notes: one hour in the machine bar, one day in Hosts. */ +import type { DaemonUpdateResult } from "../../../../shared/daemonUpdates.ts"; + +export const UPGRADE_CAPTION_MS = 60 * 60 * 1000; + +export const upgradeCaption = ( + last: DaemonUpdateResult | null, + openedAt: number | undefined, + now: number +): string | null => { + if ( + last?.result !== "updated" || + last.version === null || + now - last.at >= UPGRADE_CAPTION_MS || + (openedAt !== undefined && openedAt >= last.at) + ) + return null; + + return `Upgraded to ${last.version}`; +}; + +export const upgradeHover = ( + last: DaemonUpdateResult | null, + now: number +): { readonly title: string; readonly body: string } | null => { + if ( + last?.result !== "updated" || + last.version === null || + now - last.at >= 24 * UPGRADE_CAPTION_MS + ) + return null; + const minutes = Math.floor(Math.max(0, now - last.at) / 60_000); + + const ago = + minutes < 1 + ? "just now" + : minutes < 60 + ? `${minutes}m ago` + : `${Math.floor(minutes / 60)}h ago`; + + return { + title: `Daemon upgraded to ${last.version} · ${ago}`, + body: `${last.from === null ? "Upgraded" : `From ${last.from}`}, to match this Mac. Agent sessions kept working.`, + }; +}; diff --git a/apps/desktop/src/renderer/features/risk/ui/RiskColumn.tsx b/apps/desktop/src/renderer/features/risk/ui/RiskColumn.tsx index d93da27d..d980a2cd 100644 --- a/apps/desktop/src/renderer/features/risk/ui/RiskColumn.tsx +++ b/apps/desktop/src/renderer/features/risk/ui/RiskColumn.tsx @@ -302,7 +302,7 @@ export const RiskColumnSlot = (props: ReviewSlotProps) => { onClick={() => openSettings("hosts")} className="text-text-default cursor-default hover:underline" > - Update daemon + Upgrade daemon

    )} diff --git a/apps/desktop/src/renderer/features/settings/actions.ts b/apps/desktop/src/renderer/features/settings/actions.ts index 3aae3a37..2235cd4e 100644 --- a/apps/desktop/src/renderer/features/settings/actions.ts +++ b/apps/desktop/src/renderer/features/settings/actions.ts @@ -1,8 +1,20 @@ /** Settings' commands (`shared/keymap.ts`): "Settings…" (⌘,) and one per section, for the K menu. */ import type { CommandHandlers } from "../../routes/commands.ts"; +import { checkForUpdates, restartToUpdate, updatesStore } from "./updates/store.ts"; import type { ShellActions } from "../../routes/navigation.ts"; export const settingsCommands = (shell: ShellActions): CommandHandlers => ({ + "settings.about": { run: () => shell.openSettings("about") }, + "updates.check": { + run: () => { + shell.openSettings("about"); + void checkForUpdates(); + }, + }, + "updates.restart": { + run: () => void restartToUpdate(), + enabled: () => updatesStore.getState().view?.phase === "ready", + }, "settings.open": { run: () => shell.openSettings() }, "settings.appearance": { run: () => shell.openSettings("appearance") }, "settings.sessions": { run: () => shell.openSettings("sessions") }, diff --git a/apps/desktop/src/renderer/features/settings/model/sections.ts b/apps/desktop/src/renderer/features/settings/model/sections.ts index 685d2563..63439880 100644 --- a/apps/desktop/src/renderer/features/settings/model/sections.ts +++ b/apps/desktop/src/renderer/features/settings/model/sections.ts @@ -140,7 +140,15 @@ export const SECTION_GROUPS: ReadonlyArray = GROUPS.map((g) => ({ sections: g.sections.map((s) => s.id), })); -export const SECTIONS: ReadonlyArray = GROUPS.flatMap((g) => g.sections); +export const SECTIONS: ReadonlyArray = [ + ...GROUPS.flatMap((g) => g.sections), + { + id: "about", + title: "About", + blurb: "Polaris on this Mac.", + keywords: ["update", "version", "release", "restart"], + }, +]; const BY_ID = new Map(SECTIONS.map((s) => [s.id, s])); diff --git a/apps/desktop/src/renderer/features/settings/ui/AboutPage.tsx b/apps/desktop/src/renderer/features/settings/ui/AboutPage.tsx new file mode 100644 index 00000000..7eb20aa2 --- /dev/null +++ b/apps/desktop/src/renderer/features/settings/ui/AboutPage.tsx @@ -0,0 +1,215 @@ +/** Settings → About: quiet app Updates and the precise per-check privacy fields. */ +import { Button, Dither, FolderIcon, PixelCheckIcon, PixelPolarisIcon, Switch } from "@polaris/ui"; +import { useId } from "react"; +import { polaris } from "../../bridge.ts"; +import { useShellActions } from "../../../shell/hooks.ts"; +import { useSettings } from "../store.ts"; +import { appUpdateRow, checkedAt, type AppUpdateRow } from "../updates/model.ts"; +import { + checkForUpdates, + restartToUpdate, + setAutomaticUpdates, + showUpdateInFinder, + useUpdates, +} from "../updates/store.ts"; +import { Column, FooterStrip, Group, Heading, SettingRow } from "./parts.tsx"; + +const Glyph = ({ kind }: { readonly kind: AppUpdateRow["glyph"] }) => { + switch (kind) { + case "working": + return ; + case "check": + return ; + case "star": + return ; + case "folder": + return ; + case "failed": + return ( + + ! + + ); + case "off": + return ( + + ); + } +}; + +const ACTIONS = { check: checkForUpdates, restart: restartToUpdate, finder: showUpdateInFinder }; + +const releaseNotes = (version: string) => + void polaris().request("shell.openExternal", { + url: `https://github.com/luxdotdev/polaris/releases/tag/v${encodeURIComponent(version)}`, + }); + +export const AboutPage = () => { + const { view, problem, changing } = useUpdates((s) => s); + const version = useSettings((s) => s.version); + const { openSettings } = useShellActions(); + const id = useId(); + + if (view === null) + return ( + +

    + {problem ?? "Reading Polaris's update settings…"} +

    +
    + ); + + const row = appUpdateRow(view); + const working = view.phase === "checking" || view.phase === "downloading"; + + const fields = [ + [ + "Install ID", + view.automatic + ? `${view.installId.slice(0, 8)}…${view.installId.slice(-4)} · random, made on this Mac` + : "Not sent while automatic checks are off", + ], + ["Version", view.version], + ["Arch", view.arch], + ["macOS", view.macOSVersion], + ]; + + return ( + +
    +
    + +
    +
    +

    + Polaris {view.version || version} +

    +

    + {view.arch === "arm64" ? "Apple silicon" : view.arch} · Apache-2.0 +

    +
    +
    +
    + Updates + +
    + + + +
    + {row.title} + {row.caption} +
    +
    + {view.phase === "ready" && view.availableVersion !== null ? ( + + ) : null} + {row.action === null ? null : ( + + )} +
    +
    + + void setAutomaticUpdates(enabled)} + /> + + + {row.action === "check" ? null : ( + + )} + + +
    +

    + {view.automatic + ? "What Polaris sends with each check" + : "What Polaris sends when you check"} +

    +
    + {fields.map(([label, value]) => ( +
    +
    {label}
    +
    + {value} +
    +
    + ))} +
    +

    + Nothing else: no account, host, workspace or IP. Turning automatic checks off stops + both. +

    +
    +
    +
    + {problem === null ? null : ( +

    + {problem} +

    + )} +
    +
    + Daemons on your hosts +

    + After the update, each host's daemon follows its upgrade setting when it connects. +

    +
    + +
    +
    +
    + ); +}; diff --git a/apps/desktop/src/renderer/features/settings/ui/SettingsPage.tsx b/apps/desktop/src/renderer/features/settings/ui/SettingsPage.tsx index 14fda99f..bde7b45d 100644 --- a/apps/desktop/src/renderer/features/settings/ui/SettingsPage.tsx +++ b/apps/desktop/src/renderer/features/settings/ui/SettingsPage.tsx @@ -25,6 +25,8 @@ import { useShellActions } from "../../../shell/hooks.ts"; import { SECTION_GROUPS, sectionInfo } from "../model/sections.ts"; import { useSettings } from "../store.ts"; import { AttachmentsPage } from "../../attachments/index.ts"; +import { useUpdates } from "../updates/store.ts"; +import { AboutPage } from "./AboutPage.tsx"; import { AppearancePage } from "./AppearancePage.tsx"; import { GitHubPage } from "./GitHubPage.tsx"; import { HarnessesPage } from "./HarnessesPage.tsx"; @@ -46,6 +48,7 @@ const ICONS: Readonly> = { attachments: , reviewer: , github: , + about: , }; /** An open popover, menu, select or dialog takes esc first. */ @@ -95,6 +98,7 @@ const NavItem = ({ const SettingsNav = ({ current }: { readonly current: SettingsSection }) => { const { openSettings, closeSettings } = useShellActions(); const version = useSettings((s) => s.version); + const update = useUpdates((s) => s.view); return ( ); }; const Page = ({ route }: { readonly route: SettingsRoute }) => { switch (route.section) { + case "about": + return ; case "appearance": return ; case "sessions": diff --git a/apps/desktop/src/renderer/features/settings/updates/model.test.ts b/apps/desktop/src/renderer/features/settings/updates/model.test.ts new file mode 100644 index 00000000..693c338d --- /dev/null +++ b/apps/desktop/src/renderer/features/settings/updates/model.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "bun:test"; +import type { AppUpdateView } from "../../../../shared/appUpdates.ts"; +import { appUpdateRow } from "./model.ts"; + +const view = (patch: Partial = {}): AppUpdateView => ({ + phase: "idle", + version: "0.4.0", + availableVersion: null, + automatic: true, + lastCheckedAt: null, + installId: "58baf1dc-3772-4353-86b3-3a8a49219521", + macOSVersion: "26.1", + arch: "arm64", + supported: true, + ...patch, +}); + +describe("About Update states", () => { + test("available metadata is absent until macOS finishes downloading", () => { + expect(appUpdateRow(view({ phase: "downloading" }))).toMatchObject({ + title: "Downloading Polaris", + glyph: "working", + action: null, + }); + expect(appUpdateRow(view({ phase: "blocked" }))).toMatchObject({ + title: "Polaris can't install here", + action: "finder", + }); + }); + test("checks off still lets the user check, and retains ready updates", () => { + expect(appUpdateRow(view({ automatic: false }))).toMatchObject({ + title: "Automatic checks are off", + action: "check", + }); + expect( + appUpdateRow(view({ automatic: false, phase: "ready", availableVersion: "0.5.0" })) + ).toMatchObject({ + title: "Polaris 0.5.0 is ready", + action: "restart", + caption: "Installs when you restart or quit; agent sessions keep working", + }); + }); + test("failures stay neutral and never promise an automatic retry with checks off", () => { + expect(appUpdateRow(view({ phase: "failed", automatic: false }))).toMatchObject({ + glyph: "failed", + actionLabel: "Try again", + }); + expect(appUpdateRow(view({ phase: "failed", automatic: false })).caption).not.toContain( + "6 hours" + ); + }); + test("working states offer no second action", () => { + for (const phase of ["checking", "downloading"] as const) + expect(appUpdateRow(view({ phase })).action).toBeNull(); + }); +}); diff --git a/apps/desktop/src/renderer/features/settings/updates/model.ts b/apps/desktop/src/renderer/features/settings/updates/model.ts new file mode 100644 index 00000000..82c5cb5b --- /dev/null +++ b/apps/desktop/src/renderer/features/settings/updates/model.ts @@ -0,0 +1,96 @@ +/** Reachable update-row copy from DESIGN.md, Updates (U1–U3). */ +import type { AppUpdateView } from "../../../../shared/appUpdates.ts"; + +export interface AppUpdateRow { + readonly title: string; + readonly caption: string; + readonly glyph: "working" | "check" | "star" | "folder" | "failed" | "off"; + readonly action: "check" | "restart" | "finder" | null; + readonly actionLabel: string; +} + +export const checkedAt = (at: number | null): string => + at === null + ? "Not checked yet" + : new Date(at).toLocaleString([], { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + +const checkRow = ( + title: string, + caption: string, + glyph: AppUpdateRow["glyph"] = "check" +): AppUpdateRow => ({ title, caption, glyph, action: "check", actionLabel: "Check now" }); + +export const appUpdateRow = (view: AppUpdateView): AppUpdateRow => { + const release = view.availableVersion === null ? "Polaris" : `Polaris ${view.availableVersion}`; + + switch (view.phase) { + case "checking": + return { + title: "Checking for updates…", + caption: "Asking polaris.lux.dev for the latest release", + glyph: "working", + action: null, + actionLabel: "", + }; + case "downloading": + return { + title: `Downloading ${release}`, + caption: "In the background · nothing to do yet", + glyph: "working", + action: null, + actionLabel: "", + }; + case "ready": + return { + title: `${release} is ready`, + caption: "Installs when you restart or quit; agent sessions keep working", + glyph: "star", + action: "restart", + actionLabel: "Restart to update", + }; + case "blocked": + return { + title: `${release} can't install here`, + caption: "Move Polaris to Applications, then check again", + glyph: "folder", + action: "finder", + actionLabel: "Show in Finder", + }; + case "failed": + return { + title: "Couldn't check for updates", + caption: `polaris.lux.dev didn't answer · ${view.automatic ? "tries again in 6 hours" : "check again when you're ready"}`, + glyph: "failed", + action: "check", + actionLabel: "Try again", + }; + case "idle": + case "current": + if (!view.supported) + return checkRow( + "Updates are available in the installed Mac app", + "This build doesn't check the update feed", + "off" + ); + + if (!view.automatic) + return checkRow( + "Automatic checks are off", + `Last checked: ${checkedAt(view.lastCheckedAt)}`, + "off" + ); + + return checkRow( + view.phase === "current" ? "Polaris is up to date" : "Check for a new release", + view.lastCheckedAt === null + ? "Polaris downloads updates in the background" + : `${view.version} is the latest release · checked ${checkedAt(view.lastCheckedAt)}` + ); + } +}; diff --git a/apps/desktop/src/renderer/features/settings/updates/preview/Preview.tsx b/apps/desktop/src/renderer/features/settings/updates/preview/Preview.tsx new file mode 100644 index 00000000..72954bdf --- /dev/null +++ b/apps/desktop/src/renderer/features/settings/updates/preview/Preview.tsx @@ -0,0 +1,117 @@ +/** About on fixture Updates: every reachable phase and a fake-only interactive flow. */ +import { Schema } from "effect"; +import { createRoot } from "react-dom/client"; +import { createStore } from "zustand/vanilla"; +import type { AppEvent, PolarisApi } from "../../../../../shared/api.ts"; +import type { AppUpdateView } from "../../../../../shared/appUpdates.ts"; +import { App } from "../../../../app/App.tsx"; +import { createCommandRegistry } from "../../../../routes/commands.ts"; +import { createNavigation } from "../../../../routes/navigation.ts"; +import { type AppState, type Connection, initialState } from "../../../../store/store.ts"; +import { standInBridge } from "../../../bridge.ts"; +import { settingsCommands } from "../../actions.ts"; +import { connectUpdates, updatesStore } from "../store.ts"; + +const PHASES: ReadonlyArray = [ + "idle", + "checking", + "current", + "downloading", + "ready", + "blocked", + "failed", +]; + +const Enabled = Schema.Struct({ enabled: Schema.Boolean }); + +const fixtureBridge = (hash: string): PolarisApi => { + const scene = hash.replace(/^#updates\//, ""); + + let view: AppUpdateView = { + phase: PHASES.find((phase) => phase === scene) ?? "idle", + version: "0.4.0", + availableVersion: scene === "ready" ? "0.5.0" : null, + automatic: scene !== "off", + lastCheckedAt: Date.now() - 2 * 60 * 60_000, + installId: "58baf1dc-3772-4353-86b3-3a8a49219521", + macOSVersion: "26.1", + arch: "arm64", + supported: true, + }; + + const listeners = new Set<(event: AppEvent) => void>(); + + const publish = (patch: Partial) => { + view = { ...view, ...patch }; + + for (const listener of listeners) listener({ kind: "updates", updates: view }); + }; + + return { + request: async (method, input) => { + let value: AppUpdateView | null = null; + + switch (method) { + case "updates.get": + value = view; + break; + case "updates.setAutomatic": + publish({ automatic: Schema.decodeUnknownSync(Enabled)(input).enabled }); + value = view; + break; + case "updates.check": + publish({ phase: "checking" }); + setTimeout(() => publish({ phase: "downloading" }), 250); + setTimeout( + () => publish({ phase: "ready", availableVersion: "0.5.0", lastCheckedAt: Date.now() }), + 500 + ); + value = view; + break; + case "updates.restart": + publish({ + phase: "current", + version: view.availableVersion ?? view.version, + availableVersion: null, + }); + break; + case "updates.showInFinder": + break; + default: + return { ok: false, error: { code: "Unsupported", message: "preview" } }; + } + // SAFETY: each handled fixture method returns its documented view or null output. + + return { ok: true, value: value as never }; + }, + subscribe: () => () => undefined, + onAppEvent: (listener) => { + listeners.add(listener); + + return () => listeners.delete(listener); + }, + }; +}; + +export const mountUpdatesPreview = (root: HTMLElement, hash: string) => { + const store = createStore(() => initialState); + + const connection: Connection = { + store, + openSession: () => () => undefined, + setDensity: (density) => store.setState({ density }), + setAppearance: ({ density, theme }) => store.setState({ density, theme }), + }; + + const navigation = createNavigation({ app: store, storage: null }); + const api = fixtureBridge(hash); + standInBridge(api); + updatesStore.setState({ view: null, problem: null }); + connectUpdates(api); + navigation.actions.openSettings("about"); + const commands = createCommandRegistry({ mac: true }); + commands.register(settingsCommands(navigation.actions)); + createRoot(root).render(); + + return connection.setDensity; +}; diff --git a/apps/desktop/src/renderer/features/settings/updates/store.test.ts b/apps/desktop/src/renderer/features/settings/updates/store.test.ts new file mode 100644 index 00000000..490772b3 --- /dev/null +++ b/apps/desktop/src/renderer/features/settings/updates/store.test.ts @@ -0,0 +1,101 @@ +import { beforeEach, expect, test } from "bun:test"; +import type { AppEvent, PolarisApi, Result } from "../../../../shared/api.ts"; +import type { AppUpdateView } from "../../../../shared/appUpdates.ts"; +import { standInBridge } from "../../bridge.ts"; +import { checkForUpdates, connectUpdates, setAutomaticUpdates, updatesStore } from "./store.ts"; + +const current: AppUpdateView = { + phase: "current", + version: "0.4.0", + availableVersion: null, + automatic: true, + lastCheckedAt: 1, + installId: "58baf1dc-3772-4353-86b3-3a8a49219521", + macOSVersion: "26.1", + arch: "arm64", + supported: true, +}; + +const ready: AppUpdateView = { ...current, phase: "ready", availableVersion: "0.5.0" }; + +const deferred = () => { + let resolve: (value: Result) => void = () => undefined; + + const promise = new Promise>((done) => { + resolve = done; + }); + + return { promise, resolve }; +}; + +const fake = (delayedMethod: string) => { + const pending = deferred(); + let listener: (event: AppEvent) => void = () => undefined; + + const api: PolarisApi = { + request: (method) => { + const response = + method === delayedMethod ? pending.promise : Promise.resolve({ ok: true, value: current }); + + // SAFETY: the test only calls the three view-returning Updates methods. + return response as never; + }, + subscribe: () => () => undefined, + onAppEvent: (receive) => { + listener = receive; + + return () => { + listener = () => undefined; + }; + }, + }; + + standInBridge(api); + const stop = connectUpdates(api); + + return { + ...pending, + stop, + publish: (view: AppUpdateView) => listener({ kind: "updates", updates: view }), + }; +}; + +beforeEach(() => updatesStore.setState({ view: null, problem: null, changing: false })); + +test("a ready AppEvent wins a delayed checking reply", async () => { + const api = fake("updates.check"); + await Promise.resolve(); + const checking = checkForUpdates(); + api.publish(ready); + api.resolve({ ok: true, value: { ...current, phase: "checking" } }); + await checking; + + expect(updatesStore.getState().view).toEqual(ready); + api.stop(); +}); + +test("a later AppEvent wins the automatic-setting reply while clearing its pending control", async () => { + const api = fake("updates.setAutomatic"); + await Promise.resolve(); + const setting = setAutomaticUpdates(false); + api.publish({ ...ready, automatic: false }); + api.resolve({ ok: true, value: { ...current, automatic: false, phase: "downloading" } }); + await setting; + + expect(updatesStore.getState()).toMatchObject({ + view: { ...ready, automatic: false }, + changing: false, + problem: null, + }); + api.stop(); +}); + +test("a ready AppEvent also wins a delayed initial snapshot", async () => { + const api = fake("updates.get"); + api.publish(ready); + api.resolve({ ok: true, value: current }); + await Promise.resolve(); + + expect(updatesStore.getState().view).toEqual(ready); + api.stop(); +}); diff --git a/apps/desktop/src/renderer/features/settings/updates/store.ts b/apps/desktop/src/renderer/features/settings/updates/store.ts new file mode 100644 index 00000000..a42c4e6a --- /dev/null +++ b/apps/desktop/src/renderer/features/settings/updates/store.ts @@ -0,0 +1,80 @@ +/** Desktop App Update state comes from main; no renderer polling or success toast. */ +import { useStore } from "zustand"; +import { createStore } from "zustand/vanilla"; +import type { PolarisApi, Result } from "../../../../shared/api.ts"; +import type { AppUpdateView } from "../../../../shared/appUpdates.ts"; +import { polaris } from "../../bridge.ts"; + +interface UpdatesState { + readonly view: AppUpdateView | null; + readonly problem: string | null; + readonly changing: boolean; +} + +export const updatesStore = createStore(() => ({ + view: null, + problem: null, + changing: false, +})); + +export const useUpdates = (select: (state: UpdatesState) => A): A => + useStore(updatesStore, select); + +let revision = 0; + +const applySnapshot = (result: Result, expected: number) => { + if (revision !== expected) return; + + if (result.ok) { + revision += 1; + updatesStore.setState({ view: result.value, problem: null }); + } else updatesStore.setState({ problem: result.error.message }); +}; + +export const connectUpdates = (api: PolarisApi) => { + const expected = revision; + + const stop = api.onAppEvent((event) => { + if (event.kind === "updates") { + revision += 1; + updatesStore.setState({ view: event.updates, problem: null }); + } + }); + + void api.request("updates.get", {}).then((result) => { + applySnapshot(result, expected); + }); + + return stop; +}; + +export const checkForUpdates = async () => { + const expected = revision; + + updatesStore.setState({ problem: null }); + const result = await polaris().request("updates.check", {}); + + applySnapshot(result, expected); +}; + +export const setAutomaticUpdates = async (enabled: boolean) => { + if (updatesStore.getState().changing) return; + const expected = revision; + + updatesStore.setState({ changing: true, problem: null }); + const result = await polaris().request("updates.setAutomatic", { enabled }); + applySnapshot(result, expected); + updatesStore.setState({ changing: false }); +}; + +export const restartToUpdate = async () => { + const result = await polaris().request("updates.restart", {}); + + if (!result.ok) updatesStore.setState({ problem: result.error.message }); +}; + +export const showUpdateInFinder = async () => { + const result = await polaris().request("updates.showInFinder", {}); + + if (!result.ok) updatesStore.setState({ problem: result.error.message }); +}; diff --git a/apps/desktop/src/renderer/main.tsx b/apps/desktop/src/renderer/main.tsx index 9162e02b..b9703e6e 100644 --- a/apps/desktop/src/renderer/main.tsx +++ b/apps/desktop/src/renderer/main.tsx @@ -4,6 +4,7 @@ import { App } from "./app/App.tsx"; import { onNeedsYouEvent } from "./features/needs-you/index.ts"; import { applyAppearance } from "./appearance.ts"; import { connectSettings, settingsCommands, settingsStore } from "./features/settings/index.ts"; +import { connectUpdates } from "./features/settings/updates/store.ts"; import { createOnboarding } from "./features/onboarding/index.ts"; import { startProofSession } from "./proof.ts"; import { createCommandRegistry } from "./routes/commands.ts"; @@ -67,6 +68,8 @@ const reviewPreview = location.hash.startsWith("#review/"); const checkoutPreview = location.hash.startsWith("#checkout/"); // `#hosts/`: Settings → Hosts on fixtures, every daemon update state (features/machines/preview). +const updatesPreview = location.hash.startsWith("#updates/"); + const hostsPreview = location.hash.startsWith("#hosts/"); // `#constellation/`: the Constellation tab on fixtures (features/constellation/preview). @@ -100,6 +103,8 @@ settingsStore.subscribe((state, prev) => { connectSettings(window.polaris); +connectUpdates(window.polaris); + window.polaris.onAppEvent((event) => { if (event.kind === "command") commands.run(event.id); else if (event.kind === "needs-you") onNeedsYouEvent(event, navigation.actions); @@ -141,6 +146,10 @@ if (root !== null && preview) { void import("./features/review/checkout/preview/Preview.tsx").then((m) => { setPreviewDensity = m.mountCheckoutPreview(root, location.hash); }); +} else if (root !== null && updatesPreview) { + void import("./features/settings/updates/preview/Preview.tsx").then((m) => { + setPreviewDensity = m.mountUpdatesPreview(root, location.hash); + }); } else if (root !== null && hostsPreview) { void import("./features/machines/preview/Preview.tsx").then((m) => { setPreviewDensity = m.mountHostsPreview(root, location.hash); diff --git a/apps/desktop/src/renderer/routes/selection.ts b/apps/desktop/src/renderer/routes/selection.ts index 0d0d2b99..5016ffce 100644 --- a/apps/desktop/src/renderer/routes/selection.ts +++ b/apps/desktop/src/renderer/routes/selection.ts @@ -30,7 +30,8 @@ export type SettingsSection = | "hosts" | "attachments" | "reviewer" - | "github"; + | "github" + | "about"; export const SETTINGS_SECTIONS: ReadonlyArray = [ "appearance", @@ -43,6 +44,7 @@ export const SETTINGS_SECTIONS: ReadonlyArray = [ "attachments", "reviewer", "github", + "about", ]; /** The ⌘O dialog (DESIGN.md, Open folder): which Host it opens on; null for the selected one. */ diff --git a/apps/desktop/src/renderer/shell/TopBar.tsx b/apps/desktop/src/renderer/shell/TopBar.tsx index 02609933..46bfca2c 100644 --- a/apps/desktop/src/renderer/shell/TopBar.tsx +++ b/apps/desktop/src/renderer/shell/TopBar.tsx @@ -8,6 +8,11 @@ import { Badge, Chip, cn, Kbd, PlusIcon } from "@polaris/ui"; import { Fragment, useMemo } from "react"; import type { HostView } from "../../shared/api.ts"; import { type BarHost, barHosts, shortcutLabel } from "../routes/topBar.ts"; +import { + acknowledgeUpgrade, + UpgradeHover, + useUpgradeCaption, +} from "../features/machines/updates/UpgradeStatus.tsx"; import { slots } from "../app/slots.tsx"; import { plural } from "./copy.ts"; import { HostBarLabel, HostStateCaption } from "./HostState.tsx"; @@ -61,10 +66,12 @@ const HostLabel = ({ group }: { readonly group: BarHost }) => { const first = group.workspaces[0]; // Its first Workspace, or the Host itself when it has none (the stage to add one). - const open = (at?: number) => - first === undefined - ? selectHost(host.key, at) - : selectWorkspace({ hostKey: host.key, workspaceId: first.workspace.id }, at); + const open = (at?: number) => { + acknowledgeUpgrade(host.key); + + if (first === undefined) selectHost(host.key, at); + else selectWorkspace({ hostKey: host.key, workspaceId: first.workspace.id }, at); + }; // Needs Attention is its own bordered chip (a button); the others are a label to select the Host. if (host.status.state === "needs-attention") { @@ -76,16 +83,18 @@ const HostLabel = ({ group }: { readonly group: BarHost }) => { } return ( - + + + ); }; @@ -136,12 +145,14 @@ const WorkspaceBar = ({ bar }: { readonly bar: ReadonlyArray }) => { /** "5 workspaces", or the state when that is the news (Paper MX-0: "Slow link"). */ const MachineCaption = ({ group }: { readonly group: BarHost }) => { + const upgrade = useUpgradeCaption(group.host.key); + if (!connected(group.host) || isSlowLink(group.host)) return ; return ( - {plural(group.workspaces.length, "workspace")} + {upgrade ?? plural(group.workspaces.length, "workspace")} ); }; @@ -160,32 +171,40 @@ const MachineBar = ({ bar }: { readonly bar: ReadonlyArray }) => { const shortcut = shortcutLabel(index); return ( - + + + ); })}