Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,25 @@ jobs:
ELECTROBUN_DEVELOPER_ID: ${{ secrets.ELECTROBUN_DEVELOPER_ID }}
run: bun run desktop:release

- name: Build Homebrew app archive
run: |
set -euo pipefail
brew install zstd
tmp_dir="$(mktemp -d)"
zstd -d -o "$tmp_dir/Gloomberb.app.tar" artifacts/stable-macos-arm64-Gloomberb.app.tar.zst
tar -xf "$tmp_dir/Gloomberb.app.tar" -C "$tmp_dir"
test -x "$tmp_dir/Gloomberb.app/Contents/Resources/gloomberb"
test -f "$tmp_dir/Gloomberb.app/Contents/Resources/gloomberb-tui/tui-entry.js"
test -f "$tmp_dir/Gloomberb.app/Contents/Resources/gloomberb-tui/node_modules/@opentui/core-darwin-arm64/index.ts"
"$tmp_dir/Gloomberb.app/Contents/Resources/gloomberb" help
ditto -c -k --keepParent "$tmp_dir/Gloomberb.app" artifacts/stable-macos-arm64-Gloomberb.app.zip

- name: Verify desktop artifacts
run: |
set -euo pipefail
test -f artifacts/stable-macos-arm64-Gloomberb.dmg
test -f artifacts/stable-macos-arm64-Gloomberb.app.tar.zst
test -f artifacts/stable-macos-arm64-Gloomberb.app.zip
test -f artifacts/stable-macos-arm64-update.json
codesign --verify --verbose=4 artifacts/stable-macos-arm64-Gloomberb.dmg
xcrun stapler validate -v artifacts/stable-macos-arm64-Gloomberb.dmg
Expand Down Expand Up @@ -135,6 +149,43 @@ jobs:
with:
files: dist/*

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Update Homebrew tap
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "HOMEBREW_TAP_TOKEN is not configured; skipping Homebrew tap update."
exit 0
fi

VERSION="${GITHUB_REF_NAME#v}"
ZIP_PATH="dist/stable-macos-arm64-Gloomberb.app.zip"
SHA256="$(sha256sum "$ZIP_PATH" | awk '{print $1}')"
TAP_DIR="$RUNNER_TEMP/homebrew-tap"

git config --global user.name "gloomberb-release"
git config --global user.email "actions@users.noreply.github.com"
gh repo clone vincelwt/homebrew-tap "$TAP_DIR"
git -C "$TAP_DIR" remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/vincelwt/homebrew-tap.git"
bun run scripts/write-homebrew-cask.ts \
--version "$VERSION" \
--sha256 "$SHA256" \
--output "$TAP_DIR/Casks/gloomberb.rb"

cd "$TAP_DIR"
if git diff --quiet; then
echo "Homebrew tap already up to date."
exit 0
fi
git add Casks/gloomberb.rb
git commit -m "Update Gloomberb to $VERSION"
git push

- uses: actions/setup-node@v4
with:
node-version: 20
Expand Down
122 changes: 122 additions & 0 deletions .github/workflows/verify-desktop-homebrew.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Verify Desktop Homebrew Package

on:
workflow_dispatch:

permissions:
contents: read

jobs:
verify:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- run: bun install

- name: Configure Apple signing
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
ELECTROBUN_APPLEAPIKEY: ${{ secrets.ELECTROBUN_APPLEAPIKEY }}
ELECTROBUN_APPLEAPIKEY_BASE64: ${{ secrets.ELECTROBUN_APPLEAPIKEY_BASE64 }}
ELECTROBUN_APPLEAPIISSUER: ${{ secrets.ELECTROBUN_APPLEAPIISSUER }}
ELECTROBUN_DEVELOPER_ID: ${{ secrets.ELECTROBUN_DEVELOPER_ID }}
run: |
set -euo pipefail

: "${APPLE_CERTIFICATE_BASE64:?missing APPLE_CERTIFICATE_BASE64 secret}"
: "${APPLE_CERTIFICATE_PASSWORD:?missing APPLE_CERTIFICATE_PASSWORD secret}"
: "${APPLE_KEYCHAIN_PASSWORD:?missing APPLE_KEYCHAIN_PASSWORD secret}"
: "${ELECTROBUN_APPLEAPIKEY:?missing ELECTROBUN_APPLEAPIKEY secret}"
: "${ELECTROBUN_APPLEAPIKEY_BASE64:?missing ELECTROBUN_APPLEAPIKEY_BASE64 secret}"
: "${ELECTROBUN_APPLEAPIISSUER:?missing ELECTROBUN_APPLEAPIISSUER secret}"
: "${ELECTROBUN_DEVELOPER_ID:?missing ELECTROBUN_DEVELOPER_ID secret}"

decode_base64() {
if base64 --help 2>&1 | grep -q -- "--decode"; then
base64 --decode
else
base64 -D
fi
}

CERTIFICATE_PATH="$RUNNER_TEMP/apple-signing-certificate.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
NOTARY_KEY_PATH="$RUNNER_TEMP/AuthKey_${ELECTROBUN_APPLEAPIKEY}.p8"

printf "%s" "$APPLE_CERTIFICATE_BASE64" | decode_base64 > "$CERTIFICATE_PATH"
printf "%s" "$ELECTROBUN_APPLEAPIKEY_BASE64" | decode_base64 > "$NOTARY_KEY_PATH"

security create-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERTIFICATE_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
security find-identity -v -p codesigning "$KEYCHAIN_PATH"

echo "ELECTROBUN_APPLEAPIKEYPATH=$NOTARY_KEY_PATH" >> "$GITHUB_ENV"

- name: Build signed desktop artifacts
env:
ELECTROBUN_APPLEAPIISSUER: ${{ secrets.ELECTROBUN_APPLEAPIISSUER }}
ELECTROBUN_APPLEAPIKEY: ${{ secrets.ELECTROBUN_APPLEAPIKEY }}
ELECTROBUN_DEVELOPER_ID: ${{ secrets.ELECTROBUN_DEVELOPER_ID }}
run: bun run desktop:release

- name: Build and verify Homebrew archive
run: |
set -euo pipefail
brew install zstd
tmp_dir="$(mktemp -d)"
zstd -d -o "$tmp_dir/Gloomberb.app.tar" artifacts/stable-macos-arm64-Gloomberb.app.tar.zst
tar -xf "$tmp_dir/Gloomberb.app.tar" -C "$tmp_dir"

test -x "$tmp_dir/Gloomberb.app/Contents/Resources/gloomberb"
test -f "$tmp_dir/Gloomberb.app/Contents/Resources/gloomberb-tui/tui-entry.js"
test -f "$tmp_dir/Gloomberb.app/Contents/Resources/gloomberb-tui/node_modules/@opentui/core-darwin-arm64/index.ts"
"$tmp_dir/Gloomberb.app/Contents/Resources/gloomberb" help

codesign --verify --verbose=4 "$tmp_dir/Gloomberb.app"
ditto -c -k --keepParent "$tmp_dir/Gloomberb.app" artifacts/stable-macos-arm64-Gloomberb.app.zip

- name: Verify desktop artifacts
run: |
set -euo pipefail
test -f artifacts/stable-macos-arm64-Gloomberb.dmg
test -f artifacts/stable-macos-arm64-Gloomberb.app.tar.zst
test -f artifacts/stable-macos-arm64-Gloomberb.app.zip
test -f artifacts/stable-macos-arm64-update.json
codesign --verify --verbose=4 artifacts/stable-macos-arm64-Gloomberb.dmg
xcrun stapler validate -v artifacts/stable-macos-arm64-Gloomberb.dmg

- name: Verify cask generation and tap access
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
set -euo pipefail
: "${GH_TOKEN:?missing HOMEBREW_TAP_TOKEN secret}"

sha256="$(shasum -a 256 artifacts/stable-macos-arm64-Gloomberb.app.zip | awk '{print $1}')"
tap_dir="$RUNNER_TEMP/homebrew-tap"
gh repo clone vincelwt/homebrew-tap "$tap_dir"
git -C "$tap_dir" remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/vincelwt/homebrew-tap.git"
bun run scripts/write-homebrew-cask.ts \
--version "0.0.0" \
--sha256 "$sha256" \
--output "$tap_dir/Casks/gloomberb.rb"

ruby -c "$tap_dir/Casks/gloomberb.rb"
grep -q 'stable-macos-arm64-Gloomberb.app.zip' "$tap_dir/Casks/gloomberb.rb"
grep -q 'binary "#{appdir}/Gloomberb.app/Contents/Resources/gloomberb", target: "gloomberb"' "$tap_dir/Casks/gloomberb.rb"

cd "$tap_dir"
git add Casks/gloomberb.rb
git commit -m "Verify cask update" --allow-empty
git push --dry-run origin HEAD:homebrew-verify-${GITHUB_RUN_ID}
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,23 @@ Open the command bar with `Ctrl+P` or `` ` ``, then type a shortcut or command n

## Install

Desktop:
macOS desktop app + terminal command:

```bash
brew install --cask vincelwt/tap/gloomberb
# or
curl -fsSL gloomberb.com/install | bash
```

Both install `Gloomberb.app` and a `gloomberb` terminal command that runs the TUI through the app bundle, so the Bun runtime is stored once.

Desktop-only download:

- [Download Gloomberb for Mac](https://gloomberb.com/download/desktop)

Terminal UI:
Terminal-only install:

```bash
curl -fsSL gloomberb.com/install | bash
# or
bun install -g gloomberb
```

Expand All @@ -78,6 +86,8 @@ Then run:
gloomberb
```

On macOS, app updates replace the app bundle in place and keep the terminal command pointing at the updated bundle. Homebrew users can also update through `brew upgrade --cask gloomberb`.

For the best terminal experience, use a [Kitty](https://sw.kovidgoyal.net/kitty/)-compatible terminal such as Ghostty, Kitty, or WezTerm.

## CLI
Expand Down
2 changes: 1 addition & 1 deletion electrobun.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const config: ElectrobunConfig = {
},
scripts: {
preBuild: "scripts/build-electrobun-view.ts",
postBuild: "",
postBuild: "scripts/install-electrobun-tui-shim.ts",
postWrap: "",
postPackage: "",
},
Expand Down
94 changes: 94 additions & 0 deletions scripts/install-electrobun-tui-shim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
import { join } from "path";

const SHIM = `#!/bin/sh
set -eu

SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
CONTENTS_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
cd "$CONTENTS_DIR/MacOS"
exec "./bun" "$CONTENTS_DIR/Resources/gloomberb-tui/tui-entry.js" "$@"
`;

function appBundlesIn(dir: string): string[] {
if (!existsSync(dir)) return [];
return readdirSync(dir)
.filter((entry) => entry.endsWith(".app"))
.map((entry) => join(dir, entry));
}

function nativeFilesIn(dir: string): string[] {
if (!existsSync(dir)) return [];

return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name);
if (entry.isDirectory()) return nativeFilesIn(path);
if (entry.isFile() && (entry.name.endsWith(".dylib") || entry.name.endsWith(".node"))) return [path];
return [];
});
}

function signNativeFiles(dir: string): void {
const developerId = process.env.ELECTROBUN_DEVELOPER_ID;
if (process.platform !== "darwin" || !developerId) return;

for (const file of nativeFilesIn(dir)) {
const mode = statSync(file).mode;
chmodSync(file, mode | 0o755);

const signed = Bun.spawnSync({
cmd: ["codesign", "--force", "--timestamp", "--options", "runtime", "--sign", developerId, file],
stdout: "inherit",
stderr: "inherit",
});

if (signed.exitCode !== 0) {
process.exit(signed.exitCode ?? 1);
}
}
}

const bundlePaths = [
process.env.ELECTROBUN_WRAPPER_BUNDLE_PATH,
...appBundlesIn(process.env.ELECTROBUN_BUILD_DIR ?? ""),
].filter((value): value is string => Boolean(value));

const uniqueBundlePaths = [...new Set(bundlePaths)];
const nativeCorePackageName = `core-${process.platform}-${process.arch}`;
const nativeCorePackagePath = join(process.cwd(), "node_modules", "@opentui", nativeCorePackageName);

for (const bundlePath of uniqueBundlePaths) {
const resourcesPath = join(bundlePath, "Contents", "Resources");
if (!existsSync(resourcesPath)) continue;

const tuiBundleDir = join(resourcesPath, "gloomberb-tui");
rmSync(tuiBundleDir, { recursive: true, force: true });
mkdirSync(tuiBundleDir, { recursive: true });

const build = Bun.spawnSync({
cmd: [
process.execPath,
"build",
join(process.cwd(), "src", "renderers", "electrobun", "bun", "tui-entry.ts"),
"--target=bun",
`--outdir=${tuiBundleDir}`,
],
stdout: "inherit",
stderr: "inherit",
});

if (build.exitCode !== 0) {
process.exit(1);
}

const nativeCoreDestPath = join(tuiBundleDir, "node_modules", "@opentui", nativeCorePackageName);
rmSync(nativeCoreDestPath, { recursive: true, force: true });
mkdirSync(join(tuiBundleDir, "node_modules", "@opentui"), { recursive: true });
cpSync(nativeCorePackagePath, nativeCoreDestPath, { recursive: true, dereference: true });
signNativeFiles(nativeCoreDestPath);

const shimPath = join(resourcesPath, "gloomberb");
writeFileSync(shimPath, SHIM);
chmodSync(shimPath, 0o755);
console.log(`Installed TUI shim: ${shimPath}`);
}
Loading
Loading