Skip to content
Draft
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
152 changes: 151 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -47,8 +50,110 @@ jobs:
grep -q '^package/dist/' files.txt || { echo "::error::tarball has no dist/"; exit 1; }
if grep -Ei '\.env|secret' files.txt; then echo "::error::tarball contains secrets"; exit 1; fi

publish:
standalone:
name: Standalone (${{ matrix.platform }})
needs: verify
if: ${{ !contains(inputs.tag || github.ref_name, '-') }}
strategy:
fail-fast: false
matrix:
include:
- platform: darwin-arm64
runner: macos-14
- platform: darwin-x64
runner: macos-15-intel
- platform: linux-x64
runner: ubuntu-latest
- platform: linux-arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.tag || github.ref }}

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

- run: bun install --frozen-lockfile

- name: Build standalone runtime
env:
PUBLIC_TLDRAW_LICENSE_KEY: ${{ vars.PUBLIC_TLDRAW_LICENSE_KEY }}
run: |
[ -n "$PUBLIC_TLDRAW_LICENSE_KEY" ] || { echo "::error::PUBLIC_TLDRAW_LICENSE_KEY variable is not set"; exit 1; }
bun scripts/build-standalone.ts --outdir dist-standalone

- uses: actions/upload-artifact@v4
with:
name: moi-standalone-${{ matrix.platform }}
path: dist-standalone/moi-standalone-*
if-no-files-found: error

desktop:
name: Desktop (${{ matrix.platform }})
needs: standalone
if: ${{ !contains(inputs.tag || github.ref_name, '-') }}
strategy:
fail-fast: false
matrix:
include:
- platform: darwin-arm64
runner: macos-14
- platform: linux-x64
runner: ubuntu-latest
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.tag || github.ref }}

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

- uses: dtolnay/rust-toolchain@stable

- name: Install webview dependencies
if: startsWith(matrix.platform, 'linux')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf

- run: bun install --frozen-lockfile

- uses: actions/download-artifact@v4
with:
name: moi-standalone-${{ matrix.platform }}
path: dist-standalone

- name: Build desktop app
run: |
tarball=$(find dist-standalone -name "moi-standalone-*-${{ matrix.platform }}.tar.gz" -type f -print -quit)
test -n "$tarball"
bun scripts/build-desktop.ts --runtime "$tarball"

- uses: actions/upload-artifact@v4
with:
name: moi-desktop-${{ matrix.platform }}
path: |
desktop/src-tauri/target/release/bundle/dmg/*.dmg
desktop/src-tauri/target/release/bundle/appimage/*.AppImage
desktop/src-tauri/target/release/bundle/deb/*.deb
if-no-files-found: error

publish:
needs: [verify, standalone, desktop]
if: >-
${{
always() &&
needs.verify.result == 'success' &&
(
contains(inputs.tag || github.ref_name, '-') ||
(needs.standalone.result == 'success' && needs.desktop.result == 'success')
)
}}
runs-on: ubuntu-latest
# Gates the job on the `release` environment's reviewers, and is also the
# environment claim npm's trusted publisher config matches against.
Expand Down Expand Up @@ -96,3 +201,48 @@ jobs:
run: |
[ -n "$PUBLIC_TLDRAW_LICENSE_KEY" ] || { echo "::error::PUBLIC_TLDRAW_LICENSE_KEY variable is not set"; exit 1; }
npm publish --tag ${{ steps.meta.outputs.dist_tag }}

github-release:
name: Publish GitHub release
needs: [publish, standalone, desktop]
if: ${{ !contains(inputs.tag || github.ref_name, '-') }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.tag || github.ref }}

- uses: actions/download-artifact@v4
with:
pattern: moi-*
path: release-assets
merge-multiple: true

- name: Verify complete release payload
run: |
version=$(jq -r .version package.json)
for platform in darwin-arm64 darwin-x64 linux-x64 linux-arm64; do
asset="moi-standalone-$version-$platform.tar.gz"
test -f "release-assets/$asset"
test -f "release-assets/$asset.sha256"
(cd release-assets && sha256sum -c "$asset.sha256")
done
test "$(find release-assets -type f -name '*.dmg' | wc -l)" -ge 1
test "$(find release-assets -type f -name '*.AppImage' | wc -l)" -ge 1
test "$(find release-assets -type f -name '*.deb' | wc -l)" -ge 1

- name: Stage draft release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.tag || github.ref_name }}
draft: true
fail_on_unmatched_files: true
files: release-assets/**/*

- name: Publish complete release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
run: gh release edit "$RELEASE_TAG" --draft=false --latest
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,14 @@ server/test/__fixtures__/**/.build/

# prebuilt client (generated by prepack)
dist/

# standalone distribution artifacts (scripts/build-standalone.ts)
dist-standalone/

# desktop shell build artifacts (scripts/build-desktop.ts)
desktop/src-tauri/target/
desktop/src-tauri/resources/moi-runtime.tar.gz
desktop/src-tauri/gen/
!desktop/dist/
desktop/dist/*
!desktop/dist/.gitkeep
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Only one server runs at a time — it binds port 13337 (HTTP) and 13059 (control

All three overwrite `~/.bun/bin/moi` — last action wins; `readlink ~/.bun/bin/moi` shows which. Kill the running server before switching (single port 13337).

### Standalone distribution (no Bun required)

`docs/standalone.md` covers the fourth install form: a self-contained runtime tree under `~/.moi` (pinned bun + package + production node_modules), installed by `packaging/install.sh`, updated by `moi update` (`server/standalone.ts`), built per platform by `scripts/build-standalone.ts`, and wrapped by the Tauri desktop shell in `desktop/` (`scripts/build-desktop.ts`). Its `moi` shim lives at `~/.moi/bin/moi`, separate from `~/.bun/bin/moi`.

## Browser testing in cloud sandboxes

To drive the app in a browser inside Claude Code on the web, use the vendored **agent-browser skill** (`.agents/skills/agent-browser/`). Cloud-specific setup — server startup, `AGENT_BROWSER_EXECUTABLE_PATH`, Playwright alternative, egress-relay caveats — is in `docs/browser-testing-cloud.md`.
Expand Down
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ bun i -g moi-computer
moi start # http://localhost:13337
```

The repository also contains a no-Bun standalone CLI and desktop app for macOS
and Linux. Use them once the
[latest release](https://github.com/molefrog/moi/releases/latest) lists
`moi-standalone-*` assets; see [docs/standalone.md](docs/standalone.md). The
installer fails without changing the machine when a platform build or checksum
is absent:

```sh
curl -fsSL https://raw.githubusercontent.com/molefrog/moi/main/packaging/install.sh | sh
```

Then bring in your project, either way works:

- open [http://localhost:13337](http://localhost:13337) and create a workspace
Expand Down Expand Up @@ -110,11 +121,13 @@ moi update # update to the latest release
moi update --check # only check; exit 0 up to date, 1 update available, 2 check failed
```

`moi update` checks npm for the latest release and updates through whichever
package manager owns the install (bun, npm, pnpm, or yarn). A service-managed
server is restarted onto the new version; a foreground `moi start` only gets
a warning — restart it yourself. Prerelease installs (`…-next.N`) are left
alone. `--check` changes nothing and is made for scripts and agents.
For package-manager installs, `moi update` checks npm for the latest release and
updates through whichever package manager owns the install (bun, npm, pnpm, or
yarn). Standalone installs use the matching GitHub release asset and checksum.
A service-managed server is restarted onto the new version; a foreground
`moi start` only gets a warning — restart it yourself. Prerelease installs
(`…-next.N`) are left alone. `--check` changes nothing and is made for scripts
and agents.
`moi status` shows when the running server and CLI versions differ, however
the update happened.

Expand Down
55 changes: 55 additions & 0 deletions desktop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# moi desktop

A thin Tauri v2 shell around the standalone moi runtime. It contains no
product code: on launch it provisions the same `~/.moi` runtime tree the CLI
installer uses (extracting the bundled `moi-runtime.tar.gz` on first launch,
which also puts the `moi` command in `~/.moi/bin`), starts the `moi service` if
none is running, and opens a webview at `http://localhost:13337`.

All product iteration happens in `client/` and `server/` — a feature shipped
there appears in this app with no changes here. If a moi server is already
running (for example started with `moi start`), the app attaches to it. The
service stays available after the window closes; when service installation is
unavailable, the app falls back to an app-owned server that stops on quit.

`moi update` updates the shared runtime for both the CLI and this app.

## Visual ownership

The normal desktop window is the existing React client served by moi; the shell
does not maintain a separate application UI. `desktop/ui/index.html` is only a
temporary boot and error surface shown while the local server starts. Its build
imports the main client's Tailwind theme directly.

Desktop branding comes from `client/assets/favicon.png`. The boot page bundles
that canonical asset, and the committed platform icon set under
`src-tauri/icons/` is generated from it with
`bun scripts/build-desktop-icons.ts`. Do not add a desktop-only logo, loading
animation, palette, or type system. Product loading states after the server
starts remain owned by the client (`HomeLogo` and `LedLogo`).

## Build

Requires Rust (`rustup` or `brew install rust`) plus the platform WebView
(preinstalled on macOS; `webkit2gtk` on Linux).

```sh
bun scripts/build-desktop.ts
```

This builds the standalone runtime tarball for the current platform (or
reuses `dist-standalone/` / a `--runtime <tar.gz>` argument), stages it as the
Tauri resource, and runs `tauri build`. Bundles land in
`desktop/src-tauri/target/release/bundle/`.

## macOS Gatekeeper

Release builds are ad-hoc signed, not notarized (no Apple Developer account).
A `.dmg` downloaded with a browser is quarantined; first launch needs either
right-click → Open, or:

```sh
xattr -d com.apple.quarantine /Applications/moi.app
```

Installs via the `curl` installer are not quarantined and launch without this.
Empty file added desktop/dist/.gitkeep
Empty file.
Loading