on master help sign in
Ship the native host and a one-step installer in the release zip

The release only carried the extension, so a fresh install stopped at "Native host not installed". The build now copies host/, extension-id and scripts/install-manifest.sh (as install-host.command) into dist, so `bash <folder>/install-host.command` installs the host straight from the unzipped release. The installer finds its files from either location, explains a missing python3, and clears the quarantine flag. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LdKDAYoqmGehyHsV42LhJW

commit59dc7aa88f3ab025cebdeed9f7e41cc659a3e064 authorClaude <noreply@anthropic.com> dateWed 23 Sep 2026, 16:53 (2 weeks ago) parent98ea35d
5 files changed+40−16
.github/workflows/release.yml+1−1 README.md+21−8 scripts/build.mjs+7−1 scripts/install-manifest.sh+10−5 src/background/host.ts+1−1
.github/workflows/release.yml+1−1
@@ -54,7 +54,7 @@ jobs:
5454 VERSION: ${{ steps.version.outputs.version }}
5555 run: |
5656 asset="diagonal-extension-${VERSION}.zip"
57− notes="Built from ${GITHUB_SHA::7}. Unzip and load the folder at brave://extensions (Developer mode → Load unpacked)."
57+ notes="Built from ${GITHUB_SHA::7}. Unzip, load the folder at brave://extensions (Developer mode → Load unpacked), then install the native host once from Terminal: bash <that folder>/install-host.command"
5858 if gh release view "$VERSION" >/dev/null 2>&1; then
5959 # Two commits in the same minute share a version: refresh the asset on the existing release.
6060 gh release upload "$VERSION" "$asset" --clobber
README.md+21−8
@@ -22,17 +22,30 @@ The model sees a tab's title, address and `<meta name="description">`, and nothi
2222
2323## Install
2424
25−### 1. The extension
25+### From a release (no clone needed)
2626
27−The built extension is in `dist/` (or `diagonal-extension/` next to this folder in the delivery).
27+1. Download `diagonal-extension-<version>.zip` from the [latest release](https://github.com/iko-soy/diagonal/releases/latest) and unzip it somewhere it can stay, for example `~/Applications/Diagonal`.
28+2. Open `brave://extensions`, turn on **Developer mode**, click **Load unpacked** and pick that folder.
29+3. Install the native host once, from Terminal:
2830
29−1. Open `brave://extensions` and turn on **Developer mode**.
30−2. Click **Load unpacked** and pick the `dist/` folder.
31−3. Check that the ID shown is `mpnodlalikgeehnlnofdkpgapkmbkjdf`. It is pinned by the `key` in `manifest.json`, so it stays the same across reloads and machines.
31+ ```sh
32+ bash ~/Applications/Diagonal/install-host.command
33+ ```
34+
35+ (Type `bash `, then drag `install-host.command` from the folder into the Terminal window.) If it says python3 is not set up, run `xcode-select --install` and try again.
36+4. Open Diagonal's settings and click **Run self-test**. Brave does not need a restart.
37+
38+The zip carries the host (`host/`), `extension-id` and `install-host.command`, which is the same script as `scripts/install-manifest.sh`. When you update to a newer release, rerun step 3.
3239
33−To rebuild from source: `npm ci && npm run build`.
40+### From source
41+
42+#### 1. The extension
43+
44+1. `npm ci && npm run build` writes the unpacked extension to `dist/`.
45+2. Open `brave://extensions`, turn on **Developer mode**, click **Load unpacked** and pick `dist/`.
46+3. Check that the ID shown is `mpnodlalikgeehnlnofdkpgapkmbkjdf`. It is pinned by the `key` in `manifest.json`, so it stays the same across reloads and machines.
3447
35−### 2. The native host
48+#### 2. The native host
3649
3750Without Nix:
3851
@@ -56,7 +69,7 @@ programs.diagonal.enable = true;
5669
5770`nix build .#extension` builds `dist/` into `./result`; `nix build .#diagonal-host` builds the host.
5871
59−### 3. Check it
72+#### 3. Check it
6073
6174Open Diagonal's settings page and click **Run self-test**. Expect `fmAvailable: true`, `schemasOk: true` and a title for the three fixture tabs. From a terminal, `diagonal-host --selftest` runs the same checks.
6275
scripts/build.mjs+7−1
@@ -1,6 +1,6 @@
11// Bundles src/ into dist/ (the unpacked extension). `--watch` rebuilds on change.
22import * as esbuild from "esbuild";
3−import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3+import { chmodSync, cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
44import { dirname, join } from "node:path";
55import { fileURLToPath } from "node:url";
66
@@ -35,6 +35,12 @@ function copyStatic() {
3535 cpSync(join(root, "src/options/options.html"), join(dist, "options.html"));
3636 cpSync(join(root, "src/shared/ui.css"), join(dist, "ui.css"));
3737 cpSync(join(root, "icons"), join(dist, "icons"), { recursive: true, filter: (p) => !p.endsWith(".svg") });
38+ // The native host and its installer ride along, so the release zip is all a user needs.
39+ mkdirSync(join(dist, "host"));
40+ for (const f of ["diagonal-host.py", "prompts.py", "validate.py", "emoji.txt"]) cpSync(join(root, "host", f), join(dist, "host", f));
41+ cpSync(join(root, "extension-id"), join(dist, "extension-id"));
42+ cpSync(join(root, "scripts/install-manifest.sh"), join(dist, "install-host.command"));
43+ chmodSync(join(dist, "install-host.command"), 0o755);
3844}
3945
4046if (watch) {
scripts/install-manifest.sh+10−5
@@ -1,11 +1,14 @@
11#!/usr/bin/env bash
22# Non-Nix installer for the native host (section 14).
33# scripts/install-manifest.sh [--beta] [--nightly] [--all-channels]
4+# The release zip ships this same script as install-host.command next to host/ and extension-id,
5+# so `bash install-host.command` works from the unzipped extension folder too.
46# Copies host/ to ~/.local/share/diagonal-host, links ~/.local/bin/diagonal-host to it, writes the Brave
57# host manifest with that absolute path and the pinned extension ID, then installs the fm schemas
68# and runs the self-test. Safe to re-run.
79set -euo pipefail
8−cd "$(dirname "$0")/.."
10+HERE="$(cd "$(dirname "$0")" && pwd)"
11+if [[ -d "$HERE/host" ]]; then cd "$HERE"; else cd "$HERE/.."; fi
912
1013CHANNELS=("Brave-Browser")
1114for arg in "$@"; do
@@ -24,12 +27,12 @@ if [[ -z "$ID" ]]; then
2427fi
2528
2629PYTHON=$(command -v python3 || true)
27−if [[ -z "$PYTHON" ]]; then
28− echo "python3 not found on PATH" >&2
30+# Brave starts the host with a minimal PATH, so the shebang names this python3 explicitly.
31+# On a Mac without the Command Line Tools, /usr/bin/python3 is a stub that only offers to install them.
32+if [[ -z "$PYTHON" ]] || ! PYTHON=$("$PYTHON" -c 'import sys; print(sys.executable)' 2>/dev/null); then
33+ echo "python3 is not set up. Run: xcode-select --install then run this installer again." >&2
2934 exit 1
3035fi
31−# Brave starts the host with a minimal PATH, so the shebang names this python3 explicitly.
32−PYTHON=$("$PYTHON" -c 'import sys; print(sys.executable)')
3336
3437SHARE="$HOME/.local/share/diagonal-host"
3538BIN="$HOME/.local/bin/diagonal-host"
@@ -37,6 +40,8 @@ mkdir -p "$SHARE" "$(dirname "$BIN")"
3740cp host/prompts.py host/validate.py host/emoji.txt "$SHARE/"
3841sed -e "1s|.*|#!$PYTHON|" -e "s|^ALLOWED_ORIGIN = \".*\"|ALLOWED_ORIGIN = \"chrome-extension://$ID/\"|" host/diagonal-host.py > "$SHARE/diagonal-host.py"
3942chmod 755 "$SHARE/diagonal-host.py"
43+# Files unzipped from a browser download carry the quarantine flag; the host must not.
44+xattr -dr com.apple.quarantine "$SHARE" 2>/dev/null || true
4045ln -sf "$SHARE/diagonal-host.py" "$BIN"
4146echo "host: $BIN -> $SHARE/diagonal-host.py"
4247
src/background/host.ts+1−1
@@ -111,7 +111,7 @@ export function explain(error: HostError | undefined, extensionId = ""): string
111111 if (!error) return "";
112112 switch (error.code) {
113113 case "HOST_NOT_FOUND":
114− return `Native host not installed. Expected manifest at ${HOST_MANIFEST_PATH}`;
114+ return `Native host not installed. In Terminal, run: bash <the Diagonal folder>/install-host.command (expected manifest at ${HOST_MANIFEST_PATH})`;
115115 case "HOST_FORBIDDEN":
116116 case "FORBIDDEN_ORIGIN":
117117 return `The host does not allow this extension. Its manifest needs "allowed_origins": ["chrome-extension://${extensionId}/"]`;