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
102 changes: 23 additions & 79 deletions .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -221,92 +221,40 @@ jobs:
rt-darwin-arm64-*.tar.gz
rt-darwin-x64-*.tar.gz

# ── Update Homebrew formula ──────────────────────────────────────────────
- name: Update Homebrew tap
env:
TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
run: |
VERSION="${GITHUB_REF_NAME}"
VERSION_NUM="${VERSION#v}"
SHA_ARM=$(shasum -a 256 rt-darwin-arm64-${VERSION}.tar.gz | cut -d' ' -f1)
SHA_X64=$(shasum -a 256 rt-darwin-x64-${VERSION}.tar.gz | cut -d' ' -f1)

git clone "https://x-access-token:${TAP_TOKEN}@github.com/m4ttheweric/homebrew-tap.git" /tmp/homebrew-tap
cd /tmp/homebrew-tap

cat > Formula/rt.rb << FORMULA
class Rt < Formula
desc "Developer CLI for branch management, service runner, daemon, and notifications"
homepage "https://github.com/m4ttstack/rt"
version "${VERSION_NUM}"
license "MIT"

on_macos do
on_arm do
url "https://github.com/m4ttstack/rt/releases/download/${VERSION}/rt-darwin-arm64-${VERSION}.tar.gz"
sha256 "${SHA_ARM}"
end
on_intel do
url "https://github.com/m4ttstack/rt/releases/download/${VERSION}/rt-darwin-x64-${VERSION}.tar.gz"
sha256 "${SHA_X64}"
end
end

depends_on :macos
depends_on "fzf"
depends_on "tmux"

def install
bin.install "rt"
prefix.install "mattstack.app"
prefix.install "rt-context.vsix"
end

# No post_install hook: Homebrew runs hooks in a sandbox that can't
# write to ~/Applications, ~/.rt, or shell rc files. Setup is handled
# by the binary on first invocation (or via \`rt verify\`).

def caveats
<<~EOS
Complete setup + verify the install:
rt verify

Then configure your API tokens:
rt settings linear token
rt settings gitlab token
EOS
end
end
FORMULA

# Dedent the heredoc (remove leading spaces)
sed -i '' 's/^ //' Formula/rt.rb

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/rt.rb
git commit -m "rt ${VERSION}"
git push

# ── Post-release install test ─────────────────────────────────────────────
# Installs from the published tarball on a fresh runner, the same artifact
# mattstack.app's installer and `rt update` consume.
test-install:
needs: release
runs-on: macos-latest
steps:
- name: Install from tap
- name: Download release tarball
env:
GH_TOKEN: ${{ github.token }}
run: |
brew tap m4ttheweric/tap
brew install m4ttheweric/tap/rt
gh release download "${GITHUB_REF_NAME}" -R "${GITHUB_REPOSITORY}" \
-p "rt-darwin-arm64-*.tar.gz" -D "$RUNNER_TEMP"
mkdir -p "$RUNNER_TEMP/release"
tar -xzf "$RUNNER_TEMP"/rt-darwin-arm64-*.tar.gz -C "$RUNNER_TEMP/release"
ls -la "$RUNNER_TEMP/release"

- name: Install runtime dependencies
run: brew install fzf tmux

- name: Verify binary
run: |
rt --version
echo "Binary OK: $(rt --version)"
"$RUNNER_TEMP/release/rt" --version
echo "Binary OK: $("$RUNNER_TEMP/release/rt" --version)"

- name: Run the installer
run: |
"$RUNNER_TEMP/release/rt" --post-install
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Run post-install setup
- name: Installed binary on PATH
run: |
# Run explicitly outside the Homebrew sandbox for full user-env access
rt --post-install
test -x "$HOME/.local/bin/rt"
rt --version

- name: Install and start daemon
run: |
Expand All@@ -315,7 +263,3 @@ jobs:

- name: Verify installation
run: rt verify --ci

- name: Smoke test
run: rt --version

2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# rt — repo tools

Personal developer CLI built with Bun. Compiled to a standalone binary via `bun build --compile` and distributed through Homebrew.
Personal developer CLI built with Bun. Compiled to a standalone binary via `bun build --compile` and distributed as a GitHub Release tarball that mattstack.app (and `rt --post-install`) installs.

## Architecture docs live in Linear, not this repo

Expand Down
68 changes: 37 additions & 31 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,14 +14,21 @@ Full documentation: **https://rt.cool**

## Install

Download the latest `rt-darwin-<arch>-<version>.tar.gz` from
[GitHub Releases](https://github.com/m4ttstack/rt/releases), extract it, and run
the installer it contains:

```bash
brew install m4ttheweric/tap/rt
rt verify # completes first-run setup, then verifies everything
tar -xzf rt-darwin-arm64-*.tar.gz -C rt-release && cd rt-release
./rt --post-install # installs rt, mattstack.app, the editor extension, the daemon, shell integration
rt verify # verifies everything
```

`rt verify` runs setup on first use (tray app, daemon, editor extensions, shell
integration) and then reports the health of each piece: use it any time you
want to confirm the install is in good shape.
(The mattstack.app installer + onboarding UI that replaces this step is in
progress; `rt update` already upgrades in place from GitHub Releases.)

`rt verify` runs setup on first use and then reports the health of each piece:
use it any time you want to confirm the install is in good shape.

Then configure your API tokens:

Expand DownExpand Up@@ -232,7 +239,7 @@ rt settings linear team # Set default Linear team
rt settings gitlab token # Set GitLab personal access token
rt settings extension # Install RT Context extension into local editors
rt settings notifications # Toggle notification preferences
rt settings dev-mode # Toggle between local source and Homebrew binary
rt settings dev-mode # Toggle between local source and the installed binary
```

### Other
Expand All@@ -243,7 +250,7 @@ rt build # Interactive turbo build selector
rt hooks # Toggle git hooks on/off
rt verify # Installation verification
rt version # Print version + mode (dev/prod)
rt update # Upgrade to the latest release via Homebrew
rt update # Upgrade to the latest GitHub release
rt --version # Print version (short)
```

Expand DownExpand Up@@ -326,7 +333,7 @@ A resource missing from the file just shows its raw name and connects with Postg

## RT Context Extension

The `rt-context` VS Code/Cursor extension shows your current worktree, branch, and linked Linear ticket in the status bar. It's installed automatically by Homebrew.
The `rt-context` VS Code/Cursor extension shows your current worktree, branch, and linked Linear ticket in the status bar. The installer (`rt --post-install`) installs it into every detected editor.

To reinstall or install into additional editors:

Expand DownExpand Up@@ -364,8 +371,8 @@ From the menu you can restart the daemon, stop it, toggle launch-at-login, and c
| Dependency | Notes |
|---|---|
| macOS | Required (Apple Silicon or Intel) |
| `fzf` | 0.71.0 or newer (`--listen` and `--id-nth`, used by `rt nav`'s live refresh); auto-installed by Homebrew |
| `tmux` | Auto-installed by Homebrew |
| `fzf` | 0.71.0 or newer (`--listen` and `--id-nth`, used by `rt nav`'s live refresh); `brew install fzf` |
| `tmux` | `brew install tmux` |
| `zellij` | Optional, only needed for `rt x --zellij` mode (`brew install zellij`) |
| `chafa` | Optional, renders image previews in `rt nav` as colored character art (`brew install chafa`) |
| `kitten` | Optional, upgrades `rt nav` image previews to true pixels on Kitty-protocol terminals such as Ghostty. Ships with Kitty (`brew install --cask kitty`) |
Expand DownExpand Up@@ -393,19 +400,19 @@ bun run cli.ts verify # run any subcommand the same way

### Switching between dev and production

Once you have a Homebrew install alongside your source checkout, use the built-in toggle:
Once mattstack.app is installed alongside your source checkout, use the built-in toggle:

```bash
rt settings dev-mode # interactive picker: dev ↔ prod
rt settings dev-mode dev # switch to local source
rt settings dev-mode prod # switch back to Homebrew binary
rt settings dev-mode prod # switch back to the installed binary
```

**How it works:**
- `dev` mode writes a wrapper script at `~/.local/bin/rt` that calls `bun run /path/to/cli.ts "$@"`
- `prod` mode removes that wrapper, letting `/opt/homebrew/bin/rt` take over
- `~/.local/bin` is added to your PATH automatically (in your shell rc file) during `brew install` and on first `dev-mode dev`
- The source path is remembered in `~/.rt/dev-mode.json`, so there's no re-entry needed when toggling back
- `dev` mode writes a wrapper script at `~/.local/bin/rt` that calls `bun run /path/to/cli.ts "$@"` and hands the tray over to `mattstack-dev.app`
- `prod` mode installs the compiled binary carried inside `mattstack.app` at that same path and hands the tray back to `mattstack.app`
- `~/.local/bin` is added to your PATH automatically (in your shell rc file) by the installer and on first `dev-mode dev`
- The source path is remembered in `~/.mattstack/rt/dev-mode.json`, so there's no re-entry needed when toggling back

`rt version` tells you which is active (and the source path in dev mode);
`rt --version` is the short form that just prints the version string.
Expand All@@ -419,11 +426,13 @@ rt --post-install
```

This is the same code that `rt` auto-runs on its first invocation (and that
`rt update` re-runs after a Homebrew upgrade). It:
1. Copies `rt-tray.app` to `~/Applications`
2. Installs `rt-context.vsix` into all detected editors
3. Installs the daemon as a launchd agent
4. Writes shell integration to your rc file (PATH + rtcd, idempotent, supports zsh, bash, fish)
`rt update` runs from the freshly downloaded release). Run from an extracted
release tarball it:
1. Installs the `rt` binary at `~/.local/bin/rt`
2. Copies `mattstack.app` to `~/Applications`
3. Installs `rt-context.vsix` into all detected editors
4. Installs the daemon as a launchd agent
5. Writes shell integration to your rc file (PATH + rtcd, idempotent, supports zsh, bash, fish)

### Verifying an installation

Expand DownExpand Up@@ -484,15 +493,12 @@ git push --tags

GitHub Actions will:
1. Compile `rt` for arm64 + x64
2. Build `rt-tray.app` with version baked into `Info.plist`
2. Build `mattstack.app` with version baked into `Info.plist`
3. Package `rt-context.vsix`
4. Create a GitHub Release with bundled tarballs
5. Update `m4ttheweric/homebrew-tap` formula with real URLs + SHA256s
6. Run `rt verify --ci` on a fresh `macos-latest` runner to confirm the install works

The formula has **no** `post_install` hook: Homebrew runs formula hooks in a
sandbox that can't write to `~/Applications`, `~/.rt`, or shell rc files, so
setup is deferred to the binary itself. `cli.ts` detects a missing
`~/.rt/daemon.json` on first invocation and transparently runs
`commands/post-install.ts`, which is also what `rt --post-install` and the
post-upgrade step of `rt update` invoke.
5. Install from the published tarball on a fresh `macos-latest` runner and run `rt verify --ci`

Setup lives in the binary itself: `cli.ts` detects a missing
`~/.mattstack/rt/daemon.json` on first invocation and transparently runs
`commands/post-install.ts`, which is also what `rt --post-install` and
`rt update` invoke.
13 changes: 7 additions & 6 deletions cli.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,8 +94,9 @@ if (args[0] === "--version" || args[0] === "-V") {
const { interceptRun } = await import("./commands/intercept.ts");
await interceptRun(args.slice(2));
} else if (args[0] === "--post-install") {
// Hidden entry point: called by the Homebrew formula's post_install hook.
// Handles tray app, extension install, daemon setup, and shell integration.
// Hidden entry point: the installer. Run from an extracted release tarball
// it installs the binary, the app, the extension, the daemon, and shell
// integration; `rt update` re-execs it from the freshly downloaded release.
const { runPostInstall } = await import("./commands/post-install.ts");
await runPostInstall();
} else if (args[0] === "--grant-fda") {
Expand All@@ -121,10 +122,10 @@ if (args[0] === "--version" || args[0] === "-V") {
process.exit(0);
} else {
// ── First-run auto-setup ──────────────────────────────────────────────────
// If daemon.json doesn't exist, post-install hasn't completed outside the
// Homebrew sandbox. Run it now transparently before the requested command.
// This also applies to `rt verify`, which is the command we recommend users
// run after `brew install` — it should set up + then verify in one shot.
// If daemon.json doesn't exist, post-install has never completed. Run it
// now transparently before the requested command. This also applies to
// `rt verify`, the command we recommend after installing — it should set
// up + then verify in one shot.
if (process.env.CI !== "true" && process.env.RT_SKIP_SETUP !== "1") {
const { existsSync } = await import("fs");
const { join } = await import("path");
Expand Down
2 changes: 1 addition & 1 deletion commands/cd.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ const SHELL_FUNCTION = [
`rt() {`,
` # Resolve rt by absolute path when the dev-mode wrapper exists. This bypasses`,
` # zsh's command-hash cache, which otherwise pins rt to whichever binary it`,
` # first found (often Homebrew's compiled rt) and ignores later dev-mode`,
` # first found and ignores later dev-mode`,
` # swaps until the shell calls 'hash -r' — leading to surprising "I'm in dev`,
` # mode but my changes don't show up" behaviour across shells.`,
` local rt_bin="$HOME/.local/bin/rt"`,
Expand Down
13 changes: 4 additions & 9 deletions commands/extension.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,15 +72,10 @@ function detectEditors(): DetectedEditor[] {
// ─── VSIX Finder ─────────────────────────────────────────────────────────────

function findVsix(): string | null {
// 1. Check Homebrew formula prefix (compiled binary install)
// 1. Next to the binary (extracted release tarball layout)
const execPath = process.execPath;
const brewPrefix = resolve(execPath, "..");
const brewVsix = join(brewPrefix, "rt-context.vsix");
if (existsSync(brewVsix)) return brewVsix;

// Also check one level up (Homebrew Cellar structure: Cellar/rt/version/bin/rt)
const cellarVsix = resolve(execPath, "../../rt-context.vsix");
if (existsSync(cellarVsix)) return cellarVsix;
const bundledVsix = resolve(execPath, "../rt-context.vsix");
if (existsSync(bundledVsix)) return bundledVsix;

// 2. Check relative to source repo (development mode only — skip in compiled binary)
const metaUrl = new URL(import.meta.url).pathname;
Expand DownExpand Up@@ -118,7 +113,7 @@ export async function installExtension(): Promise<void> {
const vsixPath = findVsix();
if (!vsixPath) {
console.log(` ${red}✗${reset} rt-context.vsix not found`);
console.log(` ${dim}expected in Homebrew prefix or extensions/vscode/rt-context/${reset}\n`);
console.log(` ${dim}expected next to the rt binary or in extensions/vscode/rt-context/${reset}\n`);
return;
}

Expand Down
Loading
Loading