# The CLI and desktop downloads

> How the server hands out the kribble CLI, the editor extensions and the desktop app, how they update, how to build the desktop app, and how to run your own downloads site

Each Kribble server hands out the tools that go with it: the `kribble` command-line tool, the VS Code and JetBrains extensions and, if you add them, the desktop apps. They come from the same release as the server, so their versions match. How people use them is in [The kribble CLI](../using-kribble/the-kribble-cli.md) and [The desktop app](../using-kribble/the-desktop-app.md).

## What the server serves

`kribble-web` serves the contents of `[downloads].dir` (by default `downloads` in its working directory: `/opt/kribble/downloads` with `install.sh`, `/app/downloads` in Docker):

| Path | What |
| --- | --- |
| `/install.sh` | Shell installer for the CLI on macOS and Linux |
| `/install.ps1` | PowerShell installer for the CLI on Windows |
| `/downloads/manifest.json` | The release version and the list of files, with SHA-256 checksums |
| `/downloads/<file>` | A file listed in the manifest. Anything not listed returns 404. |
| `/downloads/<file>.sha256` | The file's checksum |
| `/downloads/jetbrains/updatePlugins.xml` | A JetBrains plugin repository for the Kribble Markdown plugin |

Signed-in users find install commands and the desktop installers on the **CLI & agents** page of the web app.

The directory comes from the release:

| Kind | Files | Built by |
| --- | --- | --- |
| CLI | `kribble-<os>-<arch>` for Linux, macOS and Windows on amd64 and arm64 | `pnpm build:release` (`dev/build.ts`), or `deploy/build-cli-downloads.sh` in the Docker image |
| VS Code extension | `kribble-markdown.vsix` | `pnpm build:release`. `KRIBBLE_SKIP_EXTENSIONS=1` skips both extensions. |
| JetBrains plugin | `kribble-intellij.zip` | `pnpm build:release`, only when a JDK and the JetBrains repositories are available. Otherwise the release is built without it and prints a warning. |
| Desktop app | installers and updater files | Built per OS and added with `node dist/add-desktop.mjs` (below) |

`install.sh` replaces `/opt/kribble/downloads` with the tarball's copy on every install and update. The Docker image built from `deploy/Dockerfile` contains only the CLI. Without the directory the server works, but offers nothing to download.

## Installing the CLI

On any computer:

```sh
curl -fsSL https://kribble.example.com/install.sh | sh     # macOS, Linux
```

```powershell
irm https://kribble.example.com/install.ps1 | iex          # Windows PowerShell
```

The script downloads the build for the computer, checks its SHA-256, installs it (`~/.local/bin`, or `/usr/local/bin` as root, or `%LOCALAPPDATA%\Programs\kribble` on Windows; `KRIBBLE_INSTALL_DIR` overrides it) and saves the server's address, so `kribble login` needs no `--server`.

The scripts contain the server's address as the client used it: the `Host` (or `X-Forwarded-Host`) and `X-Forwarded-Proto` headers your proxy passes. If they print `http://` or an internal host name, fix the proxy ([Installing](../running-a-server/installing.md)).

JetBrains IDEs can install and update the plugin from the server: add `https://kribble.example.com/downloads/jetbrains/updatePlugins.xml` under **Settings → Plugins → ⚙ → Manage Plugin Repositories**. `kribble extension install` installs the extensions from the server into VS Code-based editors and JetBrains IDEs.

## Updates

- **The CLI.** `kribble update` downloads the version the active profile's server offers, so the CLI matches the server. Without any server set up, it updates from `https://getkribble.com`. When the CLI and the server versions differ, the CLI prints a note asking the user to run `kribble update`.
- **The desktop app** checks for updates at `<server>/downloads/` of the active profile (electron-updater's generic provider, reading `latest*.yml`). Before any server is added, it uses `https://getkribble.com`. Updates download in the background and install when the app quits.

So when you deploy a new release, the CLIs and desktop apps connected to your server move to it. If the release has no desktop files, installed apps stay as they are.

## Building the desktop app

The desktop app is built separately on each OS, then added to the release tarball. Build every OS from the same commit as the server release. The app's version comes from git tags: `v1.4.0` builds 1.4.0, and later commits build `1.4.1-N`.

:::steps
::step
Build the server release

---
At the repository root:

```sh
pnpm build:release
```

This writes `dist/kribble-linux-amd64.tar.gz` and, next to it, `dist/add-desktop.mjs`.
::
::step
Build the app on each OS

---
Each needs the repository, Node, pnpm and Go:

| Command | Output in `dist-desktop/<os>/` |
| --- | --- |
| `pnpm build:desktop:mac` | dmg and zip for Apple silicon, signed and notarized when configured |
| `pnpm build:desktop:win` | the x64 installer `.exe`, unsigned. Off Windows it needs Wine, or it makes a zip that doesn't update itself. |
| `pnpm build:desktop:linux` | AppImage and deb, x64 and arm64 |
| `pnpm build:desktop:linux:docker` | the Linux build in a container, from a Mac with Docker |

Ship all the files, including the updater's (`latest*.yml`, `.blockmap`, the mac `.zip`).
::
::step
Add them to the tarball

---
Copy the files next to `dist/add-desktop.mjs` and run:

```sh
node dist/add-desktop.mjs
```

It adds them to the tarball's `downloads/` and its manifest. Adding a platform again replaces that platform's files. Then deploy the tarball as usual.
::
:::

The **Release** workflow in `.github/workflows/release.yml` does all of this on GitHub when started by hand (**Actions → Release → Run workflow**), and attaches the tarball to a draft release. It signs and notarizes the Mac app when these repository secrets are set: `MAC_CERT_P12_BASE64`, `MAC_CERT_PASSWORD`, `APPLE_API_KEY_P8`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER`.

### Signing and notarizing on a Mac

`pnpm build:desktop:mac` reads `.env` at the repository root (it is git-ignored). Without these settings it still builds, unsigned or not notarized, and says so.

| Variable | What it is |
| --- | --- |
| `CSC_NAME` | Which Developer ID Application certificate in the login keychain to use, if there are several, for example `Developer ID Application: Your Name (TEAMID1234)` |
| `CSC_LINK`, `CSC_KEY_PASSWORD` | On another machine or CI: the certificate exported as `.p12`, and its password |
| `APPLE_API_KEY`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER` | Notarization with an App Store Connect API key: the path of the `AuthKey_….p8` file, its key ID and the issuer ID (recommended) |
| `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID` | Notarization with an Apple ID and an app-specific password instead |

Signing also needs the Xcode command line tools (`xcode-select --install`). `desktop/DESKTOP.md` in the repository has the step-by-step for getting the certificate and the API key.

## Running your own downloads site

`kribble-www` is a small separate server for a public downloads site like getkribble.com: a landing page with the CLI, its install commands and the desktop apps, plus `/install.sh`, `/install.ps1`, `/downloads/*`, `llms.txt`, `sitemap.xml` and `/healthz`. It has no database or config file and serves the `downloads/` of the release it was installed from. It doesn't need a Kribble server.

Its install scripts don't set a server, so people installing from it sign in with `kribble login --server <url>`. The CLI and the desktop app use getkribble.com, which is built into them, for updates until a server is set. Your own downloads site is not used for those updates.

### On a Linux host

The release tarball includes it. Add the desktop apps to the tarball first if the site should offer them. Then:

```sh
tar xzf kribble-linux-amd64.tar.gz
sudo bash install-www.sh
```

This installs `/opt/kribble-www/kribble-www` and `/opt/kribble-www/downloads`, creates the user `kribble-www` and the unit `kribble-www.service`, and starts it on port 8080. `KRIBBLE_WWW_PORT` and `INSTALL_DIR` change the port and location. Run it again with a newer tarball to update.

`kribble-www` takes `--addr` (or `KRIBBLE_WWW_ADDR`, default `:8080`) and `--downloads` (or `KRIBBLE_WWW_DOWNLOADS`, default `downloads`). Put a TLS proxy in front that passes the host and scheme, so the install commands on the page carry the right address:

```caddyfile
downloads.example.com {
    reverse_proxy localhost:8080
}
```

### As static files on Cloudflare Pages and R2

`kribble-www --export` writes the site as static files instead: the page and install scripts for Cloudflare Pages, and the downloads, which are too big for Pages' 25 MB file limit, for an R2 bucket. `/downloads/*` on the site redirects to the bucket, and the install scripts, `kribble update` and the desktop app follow the redirect.

From a release tarball (with the desktop apps added, if wanted):

```sh
pnpm build:www                                   # uses dist/kribble-linux-amd64.tar.gz
pnpm build:www ~/Downloads/kribble-linux-amd64.tar.gz --site https://downloads.example.com --files https://dl.example.com
```

`--site` and `--files` default to `https://getkribble.com` and `https://dl.getkribble.com`. The result is `dist/www/files` (for R2) and `dist/www/site` (for Pages, with `_redirects` and `_headers`). Without `--files`, `kribble-www --export` puts the downloads into the site itself, for static hosts that accept large files.

Set up Cloudflare once:

1. Move the domain's DNS to Cloudflare.
2. Create an R2 bucket and connect the files domain (`dl.example.com`) to it as a custom domain. The `r2.dev` address doesn't go through the cache.
3. Add a Cache Rule for the files domain that makes requests eligible for cache. Cloudflare doesn't cache files without an extension, like `kribble-linux-amd64`, otherwise. Downloads served from the cache don't count as R2 reads.
4. Create a Pages project for the site's domain.
5. Optionally add a rate-limiting rule on the files domain and a billing notification. R2 has no spending cap. Past the free tier it charges for reads and storage, not traffic.

For each release, upload the files before the site, so the new page never links to files that aren't there yet:

```sh
rclone sync dist/www/files r2:<bucket> --header-upload "Cache-Control: public, max-age=300"
npx wrangler pages deploy dist/www/site --project-name <pages-project>
```

Then purge the cache, because the CLI files keep the same names from release to release. `rclone sync` also deletes the previous release's files. Files uploaded from the Cloudflare dashboard have no `Cache-Control`, so set an Edge TTL (for example an hour) in the cache rule if you upload that way.
