diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 14dba7a..49a0927 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -1,98 +1,13 @@ -name: Release CLI +# Superseded by release.yml which builds both desktop app and CLI. +# This file is kept for reference only — it does not run. +# +# To build CLI-only releases (e.g., for CI/headless), use: +# workflow_dispatch on release.yml with cli-only flag (future enhancement) +name: "[deprecated] Release CLI Only" on: - push: - tags: ['v*'] - -permissions: - contents: write - -jobs: - build: - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-darwin - artifact: syncfu-darwin-arm64 - os: macos-latest - - target: x86_64-apple-darwin - artifact: syncfu-darwin-x86_64 - os: macos-latest - - target: x86_64-unknown-linux-gnu - artifact: syncfu-linux-x86_64 - os: ubuntu-latest - - target: aarch64-unknown-linux-gnu - artifact: syncfu-linux-arm64 - os: ubuntu-latest - - target: x86_64-pc-windows-gnu - artifact: syncfu-windows-x86_64.exe - os: ubuntu-latest - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - with: - targets: ${{ matrix.target }} - - - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - with: - key: ${{ matrix.target }} - - - name: Install cargo-zigbuild and cross-compile deps - if: runner.os == 'Linux' - run: | - pip3 install ziglang - cargo install cargo-zigbuild --version 0.19.8 --locked - sudo apt-get update - sudo apt-get install -y mingw-w64 - - - name: Install ARM64 cross-linker - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - sudo apt-get install -y gcc-aarch64-linux-gnu cmake libclang-dev - echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV - - - name: Build CLI (cross-compile via zigbuild) - if: runner.os == 'Linux' - run: cargo zigbuild --release -p syncfu-cli --target ${{ matrix.target }} - - - name: Build CLI (native) - if: runner.os != 'Linux' - run: cargo build --release -p syncfu-cli --target ${{ matrix.target }} - - - name: Rename binary - run: | - if [[ "${{ matrix.target }}" == *windows* ]]; then - cp target/${{ matrix.target }}/release/syncfu.exe ${{ matrix.artifact }} - else - cp target/${{ matrix.target }}/release/syncfu ${{ matrix.artifact }} - fi - - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: ${{ matrix.artifact }} - path: ${{ matrix.artifact }} - - release: - needs: build - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - merge-multiple: true - - - name: Generate checksums - run: sha256sum syncfu-* > checksums.txt - - - name: Create release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 - with: - generate_release_notes: true - files: | - syncfu-* - checksums.txt + workflow_dispatch: + inputs: + note: + description: "Deprecated — use the main release.yml workflow instead" + required: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f1826fa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,202 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + # --- Build CLI binaries for all platforms --- + build-cli: + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-apple-darwin + artifact: syncfu-darwin-arm64 + os: macos-latest + - target: x86_64-apple-darwin + artifact: syncfu-darwin-x86_64 + os: macos-latest + - target: x86_64-unknown-linux-gnu + artifact: syncfu-linux-x86_64 + os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + artifact: syncfu-linux-arm64 + os: ubuntu-latest + - target: x86_64-pc-windows-gnu + artifact: syncfu-windows-x86_64.exe + os: ubuntu-latest + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + key: cli-${{ matrix.target }} + + - name: Install cargo-zigbuild and cross-compile deps + if: runner.os == 'Linux' + run: | + pip3 install ziglang + cargo install cargo-zigbuild --version 0.19.8 --locked + sudo apt-get update + sudo apt-get install -y mingw-w64 + + - name: Install ARM64 cross-linker + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get install -y gcc-aarch64-linux-gnu cmake libclang-dev + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + + - name: Build CLI (cross-compile via zigbuild) + if: runner.os == 'Linux' + run: cargo zigbuild --release -p syncfu-cli --target ${{ matrix.target }} + + - name: Build CLI (native) + if: runner.os != 'Linux' + run: cargo build --release -p syncfu-cli --target ${{ matrix.target }} + + - name: Rename binary + run: | + if [[ "${{ matrix.target }}" == *windows* ]]; then + cp target/${{ matrix.target }}/release/syncfu.exe ${{ matrix.artifact }} + else + cp target/${{ matrix.target }}/release/syncfu ${{ matrix.artifact }} + fi + + - uses: actions/upload-artifact@v4 + with: + name: cli-${{ matrix.artifact }} + path: ${{ matrix.artifact }} + + # --- Build desktop app for macOS, Windows, Linux --- + build-desktop: + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + target: aarch64-apple-darwin + args: --target aarch64-apple-darwin + - os: macos-latest + target: x86_64-apple-darwin + args: --target x86_64-apple-darwin + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + args: "" + - os: windows-latest + target: x86_64-pc-windows-msvc + args: "" + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + key: desktop-${{ matrix.target }} + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: desktop/pnpm-lock.yaml + + - name: Install frontend deps + working-directory: desktop + run: pnpm install --frozen-lockfile + + - name: Install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Build desktop app + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: desktop + tauriScript: pnpm tauri + args: ${{ matrix.args }} + tagName: ${{ github.ref_name }} + releaseName: "syncfu ${{ github.ref_name }}" + releaseBody: "" + releaseDraft: true + prerelease: false + includeUpdaterJson: false + + # --- Collect all artifacts into a single release --- + release: + needs: [build-cli, build-desktop] + runs-on: ubuntu-latest + + steps: + - uses: actions/download-artifact@v4 + with: + pattern: cli-* + merge-multiple: true + path: cli-artifacts + + - name: Generate checksums for CLI binaries + working-directory: cli-artifacts + run: sha256sum syncfu-* > checksums.txt + + - name: Upload CLI artifacts to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="${GITHUB_REF_NAME}" + # Wait for tauri-action to create the draft release + sleep 10 + RELEASE_ID=$(gh api repos/${{ github.repository }}/releases --jq ".[] | select(.tag_name==\"${TAG}\") | .id" | head -1) + if [ -z "$RELEASE_ID" ]; then + echo "Release not found for tag ${TAG}, creating..." + gh release create "${TAG}" --repo "${{ github.repository }}" --draft --title "syncfu ${TAG}" + fi + gh release upload "${TAG}" cli-artifacts/* --repo "${{ github.repository }}" --clobber + + - name: Publish release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="${GITHUB_REF_NAME}" + gh release edit "${TAG}" --repo "${{ github.repository }}" --draft=false \ + --notes "## Install + + **macOS / Linux (one command):** + \`\`\`sh + curl -fsSL https://raw.githubusercontent.com/${{ github.repository }}/main/install.sh | sh + \`\`\` + + **Windows (PowerShell):** + \`\`\`powershell + irm https://raw.githubusercontent.com/${{ github.repository }}/main/install.ps1 | iex + \`\`\` + + ## What's included + - **Desktop app** — tray icon, overlay notifications, HTTP server on port 9868 + - **CLI** — \`syncfu send \"hello\"\` from any terminal or AI agent + + ## Changes + See commit history for details." diff --git a/Cargo.lock b/Cargo.lock index 4bbb7bf..2d3fbc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4602,7 +4602,7 @@ dependencies = [ [[package]] name = "syncfu" -version = "0.2.0" +version = "0.3.0" dependencies = [ "anyhow", "async-stream", @@ -4631,7 +4631,7 @@ dependencies = [ [[package]] name = "syncfu-cli" -version = "0.2.0" +version = "0.3.0" dependencies = [ "anyhow", "clap", diff --git a/README.md b/README.md index e0fc089..fc3f411 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,28 @@

syncfu

-

The notification layer your AI agents are missing.

+

+ The notification layer your AI agents are missing. +

+ +

+ Release + License + Build + Platform + Rust +

+ +

+ Install · + Quick Start · + API · + Use Cases · + Integrations · + syncfu.dev +

+ +--- syncfu is a standalone overlay notification system that sits between your background processes — AI agents, autonomous loops, skills, CI pipelines, cron jobs, anything — and you. It renders always-on-top native notifications that bypass the OS notification center, so nothing gets buried. @@ -30,7 +51,42 @@ syncfu send -t "Loop complete" -p high -i circle-check \ "All 47 tests passing." ``` -Built with Tauri v2 + Rust + React. macOS first, Windows + Linux coming. +
+ +### Highlights + +| | Feature | Description | +|-|---------|-------------| +| 🔔 | **Always-on-top overlay** | NSPanel on macOS — non-activating, click-through, joins all Spaces | +| ⏳ | **`--wait` flag** | CLI blocks until the user clicks an action button (SSE-backed) | +| 🎨 | **27 style properties** | Full visual control per notification — colors, fonts, borders, radii | +| 🖥️ | **Multi-monitor** | Notifications follow your mouse cursor across displays | +| 🔗 | **Webhook callbacks** | Action buttons POST to your `callbackUrl` for closed-loop automation | +| 📊 | **Live progress bars** | Update in-flight notifications with progress, body changes, new actions | +| 🧪 | **181 tests** | 72 frontend + 70 Rust server + 29 CLI unit + 10 CLI integration | +| ⚡ | **Zero config** | No config files — everything is API-driven per notification | + +
+ +Built with **Tauri v2** + **Rust** (axum) + **React** (Zustand). macOS first, Windows + Linux coming. + +--- + +## Table of Contents + +- [Why this exists](#why-this-exists) +- [How it works](#how-it-works) +- [Install](#install) +- [Quick start](#quick-start) +- [Use cases](#use-cases) +- [System tray](#system-tray) +- [API reference](#api-reference) +- [Integrations](#integrations) +- [Architecture](#architecture) +- [Customization](#customization) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [License](#license) --- @@ -58,8 +114,6 @@ CLI ──HTTP POST──▸ syncfu server ──▸ Overlay Notification CLI (--wait) ─────────▸ SSE stream ◂──── wait for action/dismiss ``` -### Port - | Protocol | Port | Purpose | |----------|------|---------| | HTTP REST | `9868` | Send, update, dismiss, wait (SSE) | @@ -70,21 +124,24 @@ CLI (--wait) ─────────▸ SSE stream ◂──── wait for ### One-liner (recommended) +**macOS / Linux:** ```bash curl -fsSL https://raw.githubusercontent.com/Zackriya-Solutions/syncfu/main/install.sh | sh ``` -Windows (PowerShell): +**Windows (PowerShell):** ```powershell irm https://raw.githubusercontent.com/Zackriya-Solutions/syncfu/main/install.ps1 | iex ``` -Install a specific version: +**Specific version:** ```bash curl -fsSL https://raw.githubusercontent.com/Zackriya-Solutions/syncfu/main/install.sh | sh -s -- --version=0.2.0 ``` -### From source +> SHA-256 checksum verification is enforced by default. Use `--skip-checksum` to bypass. + +### From source (CLI only) ```bash git clone https://github.com/Zackriya-Solutions/syncfu.git cd syncfu @@ -99,11 +156,9 @@ pnpm install cargo tauri build ``` ---- - -## Cheatsheet +> **Prerequisites:** [Rust](https://rustup.rs/), [Node.js](https://nodejs.org/) 18+, [pnpm](https://pnpm.io/), and [Tauri v2 prerequisites](https://v2.tauri.app/start/prerequisites/). -See [CHEATSHEET.md](CHEATSHEET.md) for a quick-reference of all CLI commands, flags, and common patterns on a single page. +See [CHEATSHEET.md](CHEATSHEET.md) for a quick-reference of all CLI commands and flags. --- @@ -120,13 +175,17 @@ syncfu send "Hello from syncfu!" syncfu send -t "Build Complete" -p high "All 142 tests passing" # With action buttons -syncfu send -t "PR #42" --action "approve:Approve:primary" --action "skip:Skip:secondary" "Review requested" +syncfu send -t "PR #42" \ + --action "approve:Approve:primary" \ + --action "skip:Skip:secondary" \ + "Review requested" # Block until the user responds (--wait) -ACTION=$(syncfu send -t "Approve?" -a "yes:Yes" -a "no:No:danger" --wait "Merge PR #42?") +ACTION=$(syncfu send -t "Approve?" \ + -a "yes:Yes" -a "no:No:danger" --wait "Merge PR #42?") echo "User chose: $ACTION" # "yes", "no", "dismissed", or "timeout" -# Or use curl +# Or use curl directly curl -X POST localhost:9868/notify \ -H "Content-Type: application/json" \ -d '{"sender":"test","title":"It works","body":"Your first notification"}' @@ -144,7 +203,7 @@ syncfu is designed to stay alive in the background — your agents depend on it. - **Starts at login** (configurable) — silently, tray + overlay only - **Closing the window** hides it to the tray — doesn't quit -- **Ctrl+Q / Cmd+Q** asks: *"Quit syncfu? Agents won't be able to notify you."* — with **Quit** and **Send to Background** options +- **Ctrl+Q / Cmd+Q** asks: *"Quit syncfu? Agents won't be able to notify you."* - **Tray → Quit** also confirms before exiting - You'll never accidentally kill it @@ -152,9 +211,11 @@ syncfu is designed to stay alive in the background — your agents depend on it. ## Use cases -### AI agents & autonomous loops +
+AI agents & autonomous loops + +#### Claude Code skills and hooks -**Claude Code skills and hooks** Your `/remind` skill fires a cron, but the alert is just a terminal bell you'll never hear. Wire it to syncfu and get an overlay notification with action buttons — snooze, mark done, or open the file. ```bash @@ -164,7 +225,8 @@ syncfu send -t "$TITLE" -s remind --sound default \ "$BODY" ``` -**Autonomous coding loops** +#### Autonomous coding loops + Running `/loop` or a multi-agent workflow that takes 30 minutes? Get notified when each phase completes, when tests fail, or when the loop needs human input. ```bash @@ -173,7 +235,8 @@ syncfu send -t "Phase 3/5 complete" -s loop-operator \ "Integration tests: 42 passed, 0 failed. Starting E2E phase..." ``` -**Agent decision gates** +#### Agent decision gates + An agent needs human approval before a destructive action. Use `--wait` to block the agent until the user responds via the overlay notification. ```bash @@ -183,21 +246,15 @@ syncfu send -t "Confirm" -s agent -p high \ "Delete 47 stale branches?" && git branch -d $(git branch --merged) ``` -**Agent handoff alerts** -When one agent finishes and queues work for another, notify the human so they can review before the next agent picks it up. - -**Stalled loop detection** -A watchdog process monitors your autonomous loop and pings syncfu if no progress has been made in N minutes: *"Loop stalled — no commits in 12 minutes. Last action: running tests."* +#### More ideas +- **Agent handoff alerts** — notify the human between agent stages for review +- **Stalled loop detection** — watchdog pings syncfu if no progress in N minutes +- **Multi-agent dashboards** — each agent reports with its own sender ID, stacked as a live progress board -**Multi-agent orchestration dashboards** -Running 5 parallel agents? Each one reports status to syncfu with its own sender ID and group key. See them stacked as a live progress board on your screen. +
---- - -### CI/CD & DevOps - -**Build notifications** -GitHub Actions, GitLab CI, Jenkins — POST to syncfu when builds finish. Include pass/fail status, duration, coverage delta, and a "Open PR" action button. +
+CI/CD & DevOps ```bash syncfu send -t "Build passed" -s github-actions -i circle-check \ @@ -205,21 +262,14 @@ syncfu send -t "Build passed" -s github-actions -i circle-check \ "main built in 3m 42s — 142 tests passed, coverage 87% (+2.1%)" ``` -**Deploy progress** -Track multi-stage deployments with live progress bars that update in real-time via the update endpoint. - -**Infrastructure alerts** -Disk full, memory pressure, certificate expiring, container restart loop — get an overlay notification instead of an email you'll read tomorrow. - -**Database migration status** -Long-running migrations report progress: *"Migrating users table — 2.4M of 8.1M rows (30%)"* with a live progress bar. +- **Deploy progress** — track multi-stage deployments with live progress bars +- **Infrastructure alerts** — disk full, memory pressure, certificate expiring +- **Database migrations** — *"Migrating users table — 2.4M of 8.1M rows (30%)"* ---- - -### Development workflow +
-**Test watcher results** -`cargo watch` or `jest --watch` pipes results to syncfu. Green notification when tests pass. Red critical-priority notification when they fail — even if your terminal is buried under 14 windows. +
+Development workflow ```bash # Notify on test pass or fail @@ -227,146 +277,70 @@ cargo test && syncfu send -t "Tests passed" -p low -i circle-check "All green" \ || syncfu send -t "Tests failed" -p critical -i circle-x "Check terminal" ``` -**Long compilation finished** -Rust full rebuild? C++ linking? Go generate? Get notified when it's done instead of checking every 30 seconds. - -**PR review requested** -A webhook listener catches GitHub PR review requests and sends a syncfu notification with "Review" and "Skip" action buttons. - -**Merge conflict alerts** -Your branch just conflicted with main. A hook detects it and notifies you before you waste time building on a broken base. - -**Lint / type-check results** -Run `eslint` or `tsc --noEmit` in the background and get a clean/dirty notification overlay. +- **Long compilation finished** — Rust full rebuild, C++ linking, Go generate +- **PR review requested** — webhook listener with "Review" and "Skip" buttons +- **Merge conflict alerts** — detect before you waste time on a broken base +- **Lint / type-check results** — background `eslint` or `tsc --noEmit` ---- - -### Personal productivity & ADHD support +
-**Reminders that actually reach you** -If you have ADHD, you know: setting a reminder is useless if the reminder is a silent badge on an app you don't check. syncfu puts the reminder ON YOUR SCREEN as an unmissable overlay. +
+Personal productivity & ADHD support ```bash syncfu send -t "Stand-up in 5 minutes" -p high --sound default --timeout 300 \ "Prepare: yesterday's PR review, today's auth refactor" ``` -**Time-boxed focus sessions** -Start a 25-minute pomodoro. syncfu shows a progress bar that updates every minute. When time's up, a critical-priority notification appears: *"Pomodoro complete. Take a break."* - -**Context switching prompts** -Scheduled notifications that interrupt with context: *"You've been on this bug for 45 minutes. Current approach: checking race condition in manager.rs. Consider: stepping back and reading the test output again."* - -**Meeting prep alerts** -5 minutes before a meeting: *"Standup in 5m — you committed to reviewing the auth PR yesterday. It's still open."* - -**End-of-day review** -A nightly cron fires at 6pm: *"EOD check: 3 reminders still open, 2 PRs need review, tomorrow's first meeting is at 9am."* - -**Medication reminders** -For ADHD medication timing — a critical-priority notification with no auto-dismiss that stays on screen until you acknowledge it. Use `--wait` so the reminding system knows you actually took it. +- **Reminders that actually reach you** — overlay ON YOUR SCREEN, not a badge on an app you don't check +- **Time-boxed focus sessions** — pomodoro with live progress bar +- **Context switching prompts** — *"You've been on this bug for 45 minutes..."* +- **Medication reminders** — critical-priority, no auto-dismiss, `--wait` confirms you took it ```bash syncfu send -t "Medication" -p critical -i pill --timeout never \ -a "taken:Taken" -a "skip:Skip:danger" \ --wait "Time to take your medication" -# Blocks until user confirms — returns "taken" or "skip" ``` -**Hydration / posture / break nudges** -Recurring gentle notifications every 30-60 minutes. Low priority, auto-dismiss after 10 seconds, but enough to break the hyperfocus tunnel. - ---- - -### Server & infrastructure monitoring - -**Health check dashboard** -Poll your services every 60 seconds. Show a stacked notification group: all green, or highlight the failing service in red. +
-**SSL certificate expiry** -*"api.example.com certificate expires in 7 days"* — with a "Renew" action button that triggers your renewal script. +
+Server & infrastructure monitoring -**Disk space warnings** -*"/dev/sda1 is 92% full — 14GB remaining"* — grouped with other infra alerts. +- **Health check dashboard** — poll services every 60s, stacked notification group +- **SSL certificate expiry** — with a "Renew" action button +- **Disk space warnings** — *"/dev/sda1 is 92% full — 14GB remaining"* +- **Container restart loops** — critical priority alerts +- **Cron job completion** — nightly backup, database vacuum, log rotation -**Container restart loops** -*"api-server container has restarted 4 times in the last hour"* — critical priority. +
-**Cron job completion** -Your nightly backup, database vacuum, or log rotation finished (or failed). Know immediately. - ---- - -### Data & ML pipelines - -**Training run progress** -Long-running ML training jobs report epoch progress, loss curves (as text), and ETA. Get notified at milestones or when training completes. +
+Data & ML pipelines ```bash syncfu send -t "Epoch 45/100" -s training --progress 0.45 --group training-run-7 \ "Loss: 0.0234 (↓12%) — Val accuracy: 94.2% — ETA: 2h 15m" ``` -**Data pipeline stage completion** -ETL pipeline: extract done → transform done → load done. Each stage fires a notification. Failure at any stage fires a critical alert. - -**Model evaluation results** -*"Model v2.3 eval complete: accuracy 94.2% (+1.8%), latency p99 45ms (-12ms). Ready to deploy?"* with Deploy/Reject action buttons. - -**Dataset processing** -Processing 10M records? Get a progress bar notification that updates every 1000 records. - ---- - -### Team & collaboration - -**Slack/Discord message highlights** -A bot watches specific channels and forwards high-priority messages to syncfu. Never miss an @mention because Slack was in a background tab. - -**Email triage alerts** -Filter emails server-side and forward urgent ones as syncfu notifications: *"Email from CTO: 'Need the post-mortem by EOD'"* — with "Open Email" and "Snooze 1h" actions. - -**Shared incident response** -During an incident, a shared syncfu channel pushes updates to everyone on the team. Status changes, new findings, and action items appear as overlay notifications. - ---- - -### Home automation & IoT +- **Data pipeline stages** — extract → transform → load, with failure alerts +- **Model evaluation** — *"Model v2.3: accuracy 94.2% (+1.8%)"* with Deploy/Reject buttons +- **Dataset processing** — progress bar that updates every 1000 records -**Smart home events** -Home Assistant, Node-RED, or any IoT platform POSTs events to syncfu. Front door opened, dryer finished, garage left open. +
-**Package delivery** -Tracking webhook fires when a package is out for delivery or delivered. +
+Team, home automation & more -**Weather alerts** -Severe weather warnings pushed as high-priority notifications. +- **Slack/Discord highlights** — forward @mentions as overlay notifications +- **Email triage** — urgent emails surface as notifications with "Open" and "Snooze" actions +- **Smart home events** — Home Assistant, Node-RED, IoT webhooks +- **Price alerts** — stock/crypto threshold notifications +- **Billing warnings** — *"AWS spend: $847 (85% of $1000 budget)"* +- **Render complete** — video, 3D scene, image batch notifications ---- - -### Financial & monitoring - -**Price alerts** -Stock hits a target, crypto crosses a threshold, or a SaaS bill exceeds budget — instant overlay notification. - -**Billing threshold warnings** -*"AWS spend this month: $847 (85% of $1000 budget)"* — with a progress bar. - -**API rate limit warnings** -*"OpenAI API: 892/1000 requests used this minute"* — so you can throttle before you hit 429s. - ---- - -### Creative & content workflows - -**Render complete** -Video render, 3D scene, image generation batch — done. *"Blender render complete: 240 frames in 47 minutes."* - -**Content publishing confirmations** -Blog post deployed, social media post published, newsletter sent — confirmed via notification. - -**Transcription/processing complete** -Audio file finished transcribing, podcast episode processed, video subtitles generated. +
--- @@ -390,12 +364,12 @@ Closing the main window hides it — syncfu keeps running in the tray. The overl |--------|------|-------------| | `POST` | `/notify` | Send a notification | | `POST` | `/notify/{id}/update` | Update an existing notification (progress, body) | -| `POST` | `/notify/{id}/action` | Trigger an action (fires webhook to `callbackUrl`, dismisses) | +| `POST` | `/notify/{id}/action` | Trigger an action (fires webhook, dismisses) | | `POST` | `/notify/{id}/dismiss` | Dismiss a specific notification | -| `GET` | `/notify/{id}/wait` | SSE stream — blocks until action/dismiss (powers `--wait`) | +| `GET` | `/notify/{id}/wait` | SSE stream — blocks until action/dismiss | | `POST` | `/dismiss-all` | Dismiss all active notifications | -| `GET` | `/health` | Server status and active notification count | -| `GET` | `/active` | List all active notifications (JSON array) | +| `GET` | `/health` | Server status + active notification count | +| `GET` | `/active` | List all active notifications (JSON) | ### Notification payload @@ -419,13 +393,16 @@ Closing the main window hides it — syncfu keeps running in the tray. The overl } ``` +
+Full field reference + | Field | Type | Required | Description | |-------|------|----------|-------------| | `sender` | string | yes | Identifier for the sending process | | `title` | string | yes | Notification title | | `body` | string | yes | Body text (plain text) | | `icon` | string | no | Lucide icon name (e.g. `phone`, `git-pull-request`, `bell`) | -| `font` | string | no | Google Font name (e.g. `Space Grotesk`, `JetBrains Mono`) — loaded on demand | +| `font` | string | no | Google Font name (e.g. `Space Grotesk`, `JetBrains Mono`) | | `priority` | string | no | `low`, `normal` (default), `high`, `critical` | | `timeout` | object | no | `{"seconds": N}` or `"never"` or `"default"` (auto by priority) | | `actions` | array | no | Up to 3 action buttons | @@ -433,12 +410,15 @@ Closing the main window hides it — syncfu keeps running in the tray. The overl | `group` | string | no | Group key (tracked, grouping UI planned) | | `theme` | string | no | `light` or `dark` (auto-follows system by default) | | `sound` | string | no | Sound name (accepted, playback planned) | -| `callback_url` | string | no | URL to POST when an action button is clicked | +| `callback_url` | string | no | URL to POST when an action is clicked | | `style` | object | no | Per-notification style overrides (see below) | -### Style overrides +
+ +
+Style overrides (27 properties) -The `style` object lets you customize every visual property per notification. All fields optional: +Override any visual property per notification: ```json { @@ -462,7 +442,10 @@ The `style` object lets you customize every visual property per notification. Al Full list: `accentColor`, `cardBg`, `cardBorderRadius`, `iconColor`, `iconBg`, `iconBorderColor`, `titleColor`, `titleFontSize`, `bodyColor`, `bodyFontSize`, `senderColor`, `timeColor`, `btnBg`, `btnColor`, `btnBorderColor`, `btn2Bg`, `btn2Color`, `btn2BorderColor`, `dangerBg`, `dangerColor`, `dangerBorderColor`, `progressColor`, `progressTrackColor`, `countdownColor`, `closeBg`, `closeColor`, `closeBorderColor`. -### Per-action button styling +
+ +
+Per-action button styling Each action button can override its own colors: @@ -480,6 +463,9 @@ Each action button can override its own colors: } ] } +``` + +
--- @@ -487,7 +473,7 @@ Each action button can override its own colors: ### Claude Code hooks -Add to your `.claude/hooks.json` to get notified on every agent completion: +Get notified on every agent completion: ```json { @@ -566,11 +552,13 @@ requests.post('http://localhost:9868/notify', json={ └──────────────────────────────────┘ ``` -- **Rust backend**: axum HTTP server + SSE wait streams + waiter registry (tokio broadcast) -- **React frontend**: Zustand store, CSS animations, Lucide icons, Google Fonts -- **Overlay**: NSPanel (macOS) — non-activating, follows mouse cursor across monitors -- **CLI**: fire-and-forget by default, `--wait` opens SSE stream for blocking responses -- **System tray**: Pause, clear, settings, server status +| Component | Stack | +|-----------|-------| +| Backend | Rust — axum HTTP server, SSE streams, tokio broadcast channels | +| Frontend | React — Zustand store, CSS animations, Lucide icons, Google Fonts | +| Overlay | NSPanel (macOS) — non-activating, follows mouse cursor across monitors | +| CLI | Rust — fire-and-forget by default, `--wait` opens SSE stream | +| System tray | Tauri tray API — pause, clear, quit with confirmation | --- @@ -578,7 +566,7 @@ requests.post('http://localhost:9868/notify', json={ syncfu doesn't use config files — everything is controlled per-notification via the API. -**Per-notification styling** — override any of 27 CSS properties via the `style` field: +**Per-notification styling** — override any of 27 CSS properties: ```bash syncfu send -t "Custom" --style-json '{"accentColor":"#22c55e","cardBg":"rgba(10,40,20,0.96)"}' "Styled notification" ``` @@ -594,28 +582,26 @@ syncfu send -t "Fancy" --font "Space Grotesk" "With a custom font" ## Roadmap -- [x] Architecture & plan +### Shipped + - [x] Core overlay window + system tray - [x] NSPanel on macOS (non-activating, joins all Spaces) -- [x] Notification rendering + Liquid Glass design +- [x] Liquid Glass design + slide-in/out animations - [x] HTTP REST server (port 9868) - [x] Light/dark theme (auto + per-notification override) -- [x] Lucide icons (programmable via `icon` field) -- [x] Google Fonts (programmable via `font` field) -- [x] Slide-in/slide-out animations +- [x] Lucide icons + Google Fonts (programmable per notification) - [x] Auto-dismiss with countdown bar (pauses on hover) - [x] Critical pulsing glow (Siri-style) -- [x] Relative timestamps ("just now", "5m ago") -- [x] Priority-tinted icon containers -- [x] Dynamic panel resize (no click-blocking) - [x] Webhook callbacks (action buttons POST to `callbackUrl`) -- [x] Per-notification style overrides (27 customizable properties) -- [x] Per-action button styling (bg, color, borderColor, icon) -- [x] CLI binary (`syncfu send/dismiss/list/health`) -- [x] CLI `--wait` flag (SSE-based blocking until action/dismiss/timeout) -- [x] Multi-monitor support (notification follows mouse cursor) -- [x] 181 tests (72 frontend + 70 Rust server + 29 CLI unit + 10 CLI integration) -- [x] Click-through (overlay transparent, only cards interactive) +- [x] 27 per-notification style properties + per-action button styling +- [x] CLI (`syncfu send/dismiss/list/health`) + `--wait` flag (SSE) +- [x] Multi-monitor support (follows mouse cursor) +- [x] Click-through overlay (only cards interactive) +- [x] One-command install (`curl | sh` + PowerShell) +- [x] 181 tests (72 frontend + 70 server + 29 CLI unit + 10 CLI integration) + +### Planned + - [ ] Markdown body rendering - [ ] Sound playback - [ ] Notification grouping UI @@ -638,14 +624,16 @@ pnpm install cargo tauri dev ``` +**Prerequisites:** Rust 1.75+, Node.js 18+, pnpm, [Tauri v2 prerequisites](https://v2.tauri.app/start/prerequisites/) + --- ## License -MIT +MIT © [Zackriya Solutions](https://github.com/Zackriya-Solutions) ---

- syncfu.dev — because your agents shouldn't have to wait for you to check the terminal. + syncfu.dev — because your agents shouldn't have to wait for you to check the terminal.

diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 953fbed..fce05b0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "syncfu-cli" -version = "0.2.0" +version = "0.3.0" edition = "2021" description = "CLI for syncfu — send notifications from anywhere" license = "MIT" diff --git a/install.ps1 b/install.ps1 index ff99c20..7722b64 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,12 +1,13 @@ #Requires -Version 5.1 param( - [string]$Version = "" + [string]$Version = "", + [switch]$CliOnly ) $ErrorActionPreference = 'Stop' $Repo = "Zackriya-Solutions/syncfu" -$Artifact = "syncfu-windows-x86_64.exe" +$CliArtifact = "syncfu-windows-x86_64.exe" $BinaryName = "syncfu.exe" # --- Resolve version --- @@ -44,15 +45,16 @@ if ($Version -notmatch '^\d+\.\d+\.\d+$') { exit 1 } -# --- Install directory --- +# --- Install directories --- if ($env:SYNCFU_INSTALL_DIR) { - $InstallDir = $env:SYNCFU_INSTALL_DIR + $CliDir = $env:SYNCFU_INSTALL_DIR } else { - $InstallDir = Join-Path $HOME ".syncfu\bin" + $CliDir = Join-Path $HOME ".syncfu\bin" } -$Url = "https://github.com/$Repo/releases/download/v$Version/$Artifact" -$ChecksumUrl = "https://github.com/$Repo/releases/download/v$Version/checksums.txt" +$BaseUrl = "https://github.com/$Repo/releases/download/v$Version" +# Tauri NSIS installer artifact name +$DesktopArtifact = "syncfu_${Version}_x64-setup.exe" Write-Host "" Write-Host " syncfu installer" -ForegroundColor White @@ -61,80 +63,142 @@ Write-Host " Version: " -ForegroundColor Cyan -NoNewline Write-Host "v$Version" Write-Host " Platform: " -ForegroundColor Cyan -NoNewline Write-Host "windows/x86_64" -Write-Host " Install: " -ForegroundColor Cyan -NoNewline -Write-Host "$InstallDir" +if (-not $CliOnly) { + Write-Host " Desktop: " -ForegroundColor Cyan -NoNewline + Write-Host "yes (tray + overlay notifications)" +} +Write-Host " CLI: " -ForegroundColor Cyan -NoNewline +Write-Host "$CliDir\$BinaryName" Write-Host "" -# --- Download --- $TmpDir = Join-Path $env:TEMP "syncfu-install-$([System.IO.Path]::GetRandomFileName())" New-Item -ItemType Directory -Force -Path $TmpDir | Out-Null -$TmpFile = Join-Path $TmpDir $BinaryName try { + # ============================================= + # 1. Install desktop app (unless -CliOnly) + # ============================================= + if (-not $CliOnly) { + Write-Host "info " -ForegroundColor Green -NoNewline + Write-Host "Downloading syncfu desktop installer..." + + $InstallerPath = Join-Path $TmpDir $DesktopArtifact + try { + Invoke-WebRequest -Uri "$BaseUrl/$DesktopArtifact" -OutFile $InstallerPath -UseBasicParsing + Write-Host "info " -ForegroundColor Green -NoNewline + Write-Host "Running desktop installer (silent)..." + + # NSIS silent install + Start-Process -FilePath $InstallerPath -ArgumentList "/S" -Wait -NoNewWindow + Write-Host "info " -ForegroundColor Green -NoNewline + Write-Host "Desktop app installed" + } catch { + Write-Host "warn " -ForegroundColor Yellow -NoNewline + Write-Host "Desktop app download failed. Installing CLI only." + $CliOnly = $true + } + } + + # ============================================= + # 2. Install CLI binary + # ============================================= Write-Host "info " -ForegroundColor Green -NoNewline - Write-Host "Downloading syncfu v$Version..." + Write-Host "Downloading syncfu CLI..." - Invoke-WebRequest -Uri $Url -OutFile $TmpFile -UseBasicParsing + $CliPath = Join-Path $TmpDir $BinaryName + Invoke-WebRequest -Uri "$BaseUrl/$CliArtifact" -OutFile $CliPath -UseBasicParsing # --- Verify checksum --- Write-Host "info " -ForegroundColor Green -NoNewline Write-Host "Verifying checksum..." - try { - $ChecksumFile = Join-Path $TmpDir "checksums.txt" - Invoke-WebRequest -Uri $ChecksumUrl -OutFile $ChecksumFile -UseBasicParsing - - $EscapedArtifact = [regex]::Escape($Artifact) - $Lines = @(Get-Content $ChecksumFile | Where-Object { $_ -match "^\S+\s+$EscapedArtifact$" }) - if ($Lines.Count -ne 1) { - Write-Host "error " -ForegroundColor Red -NoNewline - Write-Host "Expected exactly 1 checksum entry for $Artifact, found $($Lines.Count)" - exit 1 - } - $Expected = ($Lines[0] -replace '\s+.*', '').ToLower() - $Actual = (Get-FileHash -Path $TmpFile -Algorithm SHA256).Hash.ToLower() + $ChecksumFile = Join-Path $TmpDir "checksums.txt" + Invoke-WebRequest -Uri "$BaseUrl/checksums.txt" -OutFile $ChecksumFile -UseBasicParsing - if ($Expected -ne $Actual) { - Write-Host "error " -ForegroundColor Red -NoNewline - Write-Host "Checksum mismatch! Expected: $Expected, Got: $Actual" - exit 1 - } - Write-Host "info " -ForegroundColor Green -NoNewline - Write-Host "Checksum verified" - } catch { + $EscapedArtifact = [regex]::Escape($CliArtifact) + $Lines = @(Get-Content $ChecksumFile | Where-Object { $_ -match "^\S+\s+$EscapedArtifact$" }) + if ($Lines.Count -ne 1) { + Write-Host "error " -ForegroundColor Red -NoNewline + Write-Host "Expected exactly 1 checksum entry for $CliArtifact, found $($Lines.Count)" + exit 1 + } + $Expected = ($Lines[0] -replace '\s+.*', '').ToLower() + $Actual = (Get-FileHash -Path $CliPath -Algorithm SHA256).Hash.ToLower() + + if ($Expected -ne $Actual) { Write-Host "error " -ForegroundColor Red -NoNewline - Write-Host "Checksum verification failed: $_" + Write-Host "Checksum mismatch! Expected: $Expected, Got: $Actual" exit 1 } + Write-Host "info " -ForegroundColor Green -NoNewline + Write-Host "Checksum verified" - # --- Install --- - New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null - Move-Item -Force $TmpFile (Join-Path $InstallDir $BinaryName) + # --- Install CLI --- + New-Item -ItemType Directory -Force -Path $CliDir | Out-Null + Move-Item -Force $CliPath (Join-Path $CliDir $BinaryName) Write-Host "info " -ForegroundColor Green -NoNewline - Write-Host "Installed to $InstallDir\$BinaryName" + Write-Host "CLI installed to $CliDir\$BinaryName" # --- Add to PATH --- $UserPath = [Environment]::GetEnvironmentVariable('Path', 'User') $PathEntries = $UserPath -split ';' - if ($InstallDir -notin $PathEntries) { - [Environment]::SetEnvironmentVariable('Path', "$InstallDir;$UserPath", 'User') - $env:Path = "$InstallDir;$env:Path" + if ($CliDir -notin $PathEntries) { + [Environment]::SetEnvironmentVariable('Path', "$CliDir;$UserPath", 'User') + $env:Path = "$CliDir;$env:Path" Write-Host "" Write-Host "info " -ForegroundColor Green -NoNewline - Write-Host "Added $InstallDir to your PATH." + Write-Host "Added $CliDir to your PATH." Write-Host " Restart your terminal for PATH changes to take effect." } + # ============================================= + # 3. Start desktop app + # ============================================= + if (-not $CliOnly) { + $SyncfuExe = Join-Path $env:LOCALAPPDATA "syncfu\syncfu.exe" + if (Test-Path $SyncfuExe) { + Write-Host "" + Write-Host "info " -ForegroundColor Green -NoNewline + Write-Host "Starting syncfu (tray + overlay)..." + Start-Process -FilePath $SyncfuExe -WindowStyle Hidden + + Start-Sleep -Seconds 3 + try { + $null = Invoke-WebRequest -Uri "http://127.0.0.1:9868/health" -UseBasicParsing -TimeoutSec 2 + Write-Host "info " -ForegroundColor Green -NoNewline + Write-Host "syncfu is running - server listening on port 9868" + } catch { + Write-Host "warn " -ForegroundColor Yellow -NoNewline + Write-Host "syncfu started but server not yet responding. It may take a moment." + } + } + } + # --- Done --- Write-Host "" + if (-not $CliOnly) { + Write-Host " syncfu is installed and running!" -ForegroundColor White + Write-Host "" + Write-Host " Quick test:" -ForegroundColor Cyan + Write-Host ' syncfu send "Hello from syncfu!"' + } else { + Write-Host " syncfu CLI installed (headless mode)." -ForegroundColor White + Write-Host "" + Write-Host " Start the server:" -ForegroundColor Cyan + Write-Host " syncfu serve" + Write-Host "" + Write-Host " Send a notification:" -ForegroundColor Cyan + Write-Host ' syncfu send "Hello from syncfu!"' + } + Write-Host "" Write-Host "info " -ForegroundColor Green -NoNewline Write-Host "Done! Run 'syncfu --help' to get started." Write-Host "" } catch { Write-Host "error " -ForegroundColor Red -NoNewline - Write-Host "Download failed. Check: https://github.com/$Repo/releases/tag/v$Version" + Write-Host "Installation failed: $_" exit 1 } finally { Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue diff --git a/install.sh b/install.sh index 70ff90b..4569aa4 100755 --- a/install.sh +++ b/install.sh @@ -2,34 +2,37 @@ set -eu REPO="Zackriya-Solutions/syncfu" -BINARY_NAME="syncfu" +APP_NAME="syncfu" SKIP_CHECKSUM=0 # --- Colors (only if terminal) --- if [ -t 1 ]; then - BOLD='\033[1m' GREEN='\033[32m' CYAN='\033[36m' RED='\033[31m' RESET='\033[0m' + BOLD='\033[1m' GREEN='\033[32m' CYAN='\033[36m' RED='\033[31m' YELLOW='\033[33m' RESET='\033[0m' else - BOLD='' GREEN='' CYAN='' RED='' RESET='' + BOLD='' GREEN='' CYAN='' RED='' YELLOW='' RESET='' fi info() { printf "${GREEN}info${RESET} %s\n" "$@"; } -warn() { printf "${CYAN}warn${RESET} %s\n" "$@"; } +warn() { printf "${YELLOW}warn${RESET} %s\n" "$@"; } error() { printf "${RED}error${RESET} %s\n" "$@" >&2; exit 1; } # --- Parse arguments --- VERSION="" +CLI_ONLY=0 for arg in "$@"; do case "$arg" in --version=*) VERSION="${arg#--version=}" ;; --skip-checksum) SKIP_CHECKSUM=1 ;; + --cli-only) CLI_ONLY=1 ;; --help|-h) - printf "Usage: install.sh [--version=X.Y.Z] [--skip-checksum]\n" - printf "\nInstalls the syncfu CLI from GitHub Releases.\n" + printf "Usage: install.sh [--version=X.Y.Z] [--skip-checksum] [--cli-only]\n" + printf "\nInstalls the syncfu desktop app and CLI.\n" printf "\nOptions:\n" printf " --version=X.Y.Z Install a specific version (default: latest)\n" - printf " --skip-checksum Skip SHA-256 integrity verification\n" + printf " --skip-checksum Skip SHA-256 integrity verification for CLI binary\n" + printf " --cli-only Install only the CLI (no desktop app, headless mode)\n" printf "\nEnvironment:\n" - printf " SYNCFU_INSTALL_DIR Override install directory (must be absolute path)\n" + printf " SYNCFU_INSTALL_DIR Override CLI install directory (must be absolute path)\n" exit 0 ;; esac @@ -53,10 +56,8 @@ case "$ARCH" in *) error "Unsupported architecture: $ARCH" ;; esac -ARTIFACT="${BINARY_NAME}-${OS_NAME}-${ARCH_NAME}" - # --- Require curl --- -command -v curl >/dev/null 2>&1 || error "curl is required but not installed. Install it with your package manager." +command -v curl >/dev/null 2>&1 || error "curl is required but not installed." # --- Resolve version --- if [ -z "$VERSION" ]; then @@ -71,43 +72,146 @@ if [ -z "$VERSION" ]; then fi fi -# --- Validate version format (digits and dots only) --- +# --- Validate version format --- if ! expr "$VERSION" : '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' > /dev/null 2>&1; then error "Unexpected version format: $VERSION" fi -# --- Install directory --- +# --- CLI install directory --- if [ -n "${SYNCFU_INSTALL_DIR:-}" ]; then case "$SYNCFU_INSTALL_DIR" in /*) ;; *) error "SYNCFU_INSTALL_DIR must be an absolute path" ;; esac - INSTALL_DIR="$SYNCFU_INSTALL_DIR" + CLI_DIR="$SYNCFU_INSTALL_DIR" elif [ -w /usr/local/bin ]; then - INSTALL_DIR="/usr/local/bin" + CLI_DIR="/usr/local/bin" else - INSTALL_DIR="$HOME/.syncfu/bin" + CLI_DIR="$HOME/.syncfu/bin" fi -# --- Download --- -URL="https://github.com/${REPO}/releases/download/v${VERSION}/${ARTIFACT}" -CHECKSUM_URL="https://github.com/${REPO}/releases/download/v${VERSION}/checksums.txt" - +# --- Work directory --- WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/syncfu-install.XXXXXXXXXX") trap 'rm -rf "$WORK_DIR"' EXIT +# --- Artifact names --- +CLI_ARTIFACT="${APP_NAME}-${OS_NAME}-${ARCH_NAME}" + +if [ "$OS_NAME" = "darwin" ]; then + # Tauri produces: syncfu_0.2.0_aarch64.dmg / syncfu_0.2.0_x86_64.dmg + if [ "$ARCH_NAME" = "arm64" ]; then + TAURI_ARCH="aarch64" + else + TAURI_ARCH="x86_64" + fi + DESKTOP_ARTIFACT="${APP_NAME}_${VERSION}_${TAURI_ARCH}.dmg" +elif [ "$OS_NAME" = "linux" ]; then + DESKTOP_ARTIFACT="${APP_NAME}_${VERSION}_amd64.AppImage" +fi + +BASE_URL="https://github.com/${REPO}/releases/download/v${VERSION}" + printf "\n" printf " ${BOLD}syncfu${RESET} installer\n" printf "\n" -printf " ${CYAN}Version:${RESET} v%s\n" "$VERSION" -printf " ${CYAN}Platform:${RESET} %s/%s\n" "$OS_NAME" "$ARCH_NAME" -printf " ${CYAN}Install:${RESET} %s\n" "$INSTALL_DIR" +printf " ${CYAN}Version:${RESET} v%s\n" "$VERSION" +printf " ${CYAN}Platform:${RESET} %s/%s\n" "$OS_NAME" "$ARCH_NAME" +if [ "$CLI_ONLY" = "0" ]; then + printf " ${CYAN}Desktop:${RESET} yes (tray + overlay notifications)\n" +fi +printf " ${CYAN}CLI:${RESET} %s/syncfu\n" "$CLI_DIR" printf "\n" -info "Downloading syncfu v${VERSION}..." -HTTP_CODE=$(curl -sL -w '%{http_code}' -o "${WORK_DIR}/${BINARY_NAME}" "$URL" 2>/dev/null || true) +# ============================================= +# 1. Install desktop app (unless --cli-only) +# ============================================= +if [ "$CLI_ONLY" = "0" ]; then + if [ "$OS_NAME" = "darwin" ]; then + install_macos_app() { + info "Downloading syncfu desktop app..." + DMG_PATH="${WORK_DIR}/${DESKTOP_ARTIFACT}" + HTTP_CODE=$(curl -sL -w '%{http_code}' -o "$DMG_PATH" "${BASE_URL}/${DESKTOP_ARTIFACT}" 2>/dev/null || true) + if [ "$HTTP_CODE" != "200" ]; then + warn "Desktop app download failed (HTTP ${HTTP_CODE:-???}). Falling back to CLI-only." + return 1 + fi + + info "Installing syncfu.app to /Applications..." + MOUNT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/syncfu-dmg.XXXXXXXXXX") + + hdiutil attach "$DMG_PATH" -nobrowse -mountpoint "$MOUNT_DIR" -quiet 2>/dev/null + if [ -d "$MOUNT_DIR/syncfu.app" ]; then + # Remove old version if present + if [ -d "/Applications/syncfu.app" ]; then + rm -rf "/Applications/syncfu.app" + fi + cp -R "$MOUNT_DIR/syncfu.app" "/Applications/syncfu.app" + # Remove quarantine + xattr -rd com.apple.quarantine "/Applications/syncfu.app" 2>/dev/null || true + info "Installed syncfu.app to /Applications" + else + hdiutil detach "$MOUNT_DIR" -quiet 2>/dev/null || true + rm -rf "$MOUNT_DIR" + warn "Could not find syncfu.app in DMG. Falling back to CLI-only." + return 1 + fi + + hdiutil detach "$MOUNT_DIR" -quiet 2>/dev/null || true + rm -rf "$MOUNT_DIR" + return 0 + } + + install_macos_app || CLI_ONLY=1 + + elif [ "$OS_NAME" = "linux" ]; then + install_linux_app() { + info "Downloading syncfu AppImage..." + APPIMAGE_PATH="${WORK_DIR}/${DESKTOP_ARTIFACT}" + HTTP_CODE=$(curl -sL -w '%{http_code}' -o "$APPIMAGE_PATH" "${BASE_URL}/${DESKTOP_ARTIFACT}" 2>/dev/null || true) + if [ "$HTTP_CODE" != "200" ]; then + warn "Desktop app download failed (HTTP ${HTTP_CODE:-???}). Falling back to CLI-only." + return 1 + fi + + APPIMAGE_DIR="$HOME/.local/share/syncfu" + mkdir -p "$APPIMAGE_DIR" + chmod +x "$APPIMAGE_PATH" + mv "$APPIMAGE_PATH" "$APPIMAGE_DIR/syncfu.AppImage" + info "Installed syncfu AppImage to $APPIMAGE_DIR" + + # Create desktop entry + DESKTOP_DIR="$HOME/.local/share/applications" + mkdir -p "$DESKTOP_DIR" + cat > "$DESKTOP_DIR/syncfu.desktop" << 'DESKTOP_EOF' +[Desktop Entry] +Name=syncfu +Comment=Universal notification overlay +Exec=$HOME/.local/share/syncfu/syncfu.AppImage +Icon=syncfu +Type=Application +Categories=Utility; +StartupNotify=false +DESKTOP_EOF + # Fix the Exec path (can't use variable inside heredoc with single quotes) + sed -i "s|\$HOME|$HOME|g" "$DESKTOP_DIR/syncfu.desktop" + info "Created desktop entry" + return 0 + } + + install_linux_app || CLI_ONLY=1 + fi +fi + +# ============================================= +# 2. Install CLI binary +# ============================================= +info "Downloading syncfu CLI..." +CLI_URL="${BASE_URL}/${CLI_ARTIFACT}" +CHECKSUM_URL="${BASE_URL}/checksums.txt" + +HTTP_CODE=$(curl -sL -w '%{http_code}' -o "${WORK_DIR}/syncfu" "$CLI_URL" 2>/dev/null || true) if [ "$HTTP_CODE" != "200" ]; then - error "Download failed (HTTP ${HTTP_CODE:-???}). Check: https://github.com/${REPO}/releases/tag/v${VERSION}" + error "CLI download failed (HTTP ${HTTP_CODE:-???}). Check: https://github.com/${REPO}/releases/tag/v${VERSION}" fi # --- Verify checksum --- @@ -118,14 +222,14 @@ else if ! curl -fsSL -o "${WORK_DIR}/checksums.txt" "$CHECKSUM_URL" 2>/dev/null; then error "Could not download checksums.txt — cannot verify integrity. Use --skip-checksum to bypass." fi - EXPECTED=$(grep -F "${ARTIFACT}" "${WORK_DIR}/checksums.txt" | awk '{print $1}') + EXPECTED=$(grep -F "${CLI_ARTIFACT}" "${WORK_DIR}/checksums.txt" | awk '{print $1}') if [ -z "$EXPECTED" ]; then - error "Artifact '${ARTIFACT}' not found in checksums.txt. Use --skip-checksum to bypass." + error "Artifact '${CLI_ARTIFACT}' not found in checksums.txt. Use --skip-checksum to bypass." fi if command -v sha256sum >/dev/null 2>&1; then - ACTUAL=$(sha256sum "${WORK_DIR}/${BINARY_NAME}" | awk '{print $1}') + ACTUAL=$(sha256sum "${WORK_DIR}/syncfu" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then - ACTUAL=$(shasum -a 256 "${WORK_DIR}/${BINARY_NAME}" | awk '{print $1}') + ACTUAL=$(shasum -a 256 "${WORK_DIR}/syncfu" | awk '{print $1}') else error "No sha256sum or shasum found — cannot verify integrity. Use --skip-checksum to bypass." fi @@ -135,49 +239,101 @@ else info "Checksum verified" fi -# --- Install --- -chmod +x "${WORK_DIR}/${BINARY_NAME}" - -# Remove macOS quarantine attribute +# --- Install CLI --- +chmod +x "${WORK_DIR}/syncfu" if [ "$OS_NAME" = "darwin" ]; then - xattr -d com.apple.quarantine "${WORK_DIR}/${BINARY_NAME}" 2>/dev/null || true + xattr -d com.apple.quarantine "${WORK_DIR}/syncfu" 2>/dev/null || true fi - -mkdir -p "$INSTALL_DIR" -mv "${WORK_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}" - -info "Installed to ${INSTALL_DIR}/${BINARY_NAME}" +mkdir -p "$CLI_DIR" +mv "${WORK_DIR}/syncfu" "${CLI_DIR}/syncfu" +info "CLI installed to ${CLI_DIR}/syncfu" # --- PATH instructions --- case ":${PATH:-}:" in - *":${INSTALL_DIR}:"*) - # Already in PATH + *":${CLI_DIR}:"*) ;; *) printf "\n" warn "Add syncfu to your PATH:" printf "\n" - SHELL_NAME=$(basename "${SHELL:-/bin/sh}") case "$SHELL_NAME" in - zsh) - printf " echo 'export PATH=\"%s:\$PATH\"' >> ~/.zshrc && source ~/.zshrc\n" "$INSTALL_DIR" - ;; - bash) - printf " echo 'export PATH=\"%s:\$PATH\"' >> ~/.bashrc && source ~/.bashrc\n" "$INSTALL_DIR" - ;; - fish) - printf " fish_add_path %s\n" "$INSTALL_DIR" - ;; - *) - printf " export PATH=\"%s:\$PATH\"\n" "$INSTALL_DIR" - ;; + zsh) printf " echo 'export PATH=\"%s:\$PATH\"' >> ~/.zshrc && source ~/.zshrc\n" "$CLI_DIR" ;; + bash) printf " echo 'export PATH=\"%s:\$PATH\"' >> ~/.bashrc && source ~/.bashrc\n" "$CLI_DIR" ;; + fish) printf " fish_add_path %s\n" "$CLI_DIR" ;; + *) printf " export PATH=\"%s:\$PATH\"\n" "$CLI_DIR" ;; esac printf "\n" ;; esac +# ============================================= +# 3. Start desktop app (tray only, no window) +# ============================================= +if [ "$CLI_ONLY" = "0" ]; then + if [ "$OS_NAME" = "darwin" ] && [ -d "/Applications/syncfu.app" ]; then + info "Starting syncfu (tray + overlay)..." + open -a "/Applications/syncfu.app" + # Wait briefly for the server to come up + for i in 1 2 3 4 5 6 7 8; do + sleep 0.5 + if curl -s "http://127.0.0.1:9868/health" >/dev/null 2>&1; then + break + fi + done + + if curl -s "http://127.0.0.1:9868/health" >/dev/null 2>&1; then + info "syncfu is running — server listening on port 9868" + else + warn "syncfu app started but server not yet responding. It may take a moment." + fi + + elif [ "$OS_NAME" = "linux" ]; then + APPIMAGE="$HOME/.local/share/syncfu/syncfu.AppImage" + if [ -x "$APPIMAGE" ]; then + info "Starting syncfu (tray + overlay)..." + nohup "$APPIMAGE" >/dev/null 2>&1 & + sleep 2 + if curl -s "http://127.0.0.1:9868/health" >/dev/null 2>&1; then + info "syncfu is running — server listening on port 9868" + else + warn "syncfu started but server not yet responding. It may take a moment." + fi + fi + fi +fi + +# --- Verify CLI --- +SYNCFU_BIN="${CLI_DIR}/syncfu" +if [ -x "$SYNCFU_BIN" ]; then + INSTALLED_VERSION=$("$SYNCFU_BIN" --version 2>/dev/null || true) + if [ -n "$INSTALLED_VERSION" ]; then + info "$INSTALLED_VERSION" + fi +fi + # --- Done --- printf "\n" +if [ "$CLI_ONLY" = "0" ]; then + printf " ${BOLD}syncfu is installed and running!${RESET}\n" + printf "\n" + printf " ${CYAN}Quick test:${RESET}\n" + printf " syncfu send \"Hello from syncfu!\"\n" + printf "\n" + printf " The desktop app runs in the system tray. Notifications appear\n" + printf " as overlay panels — no focus stealing, works across all Spaces.\n" +else + printf " ${BOLD}syncfu CLI installed (headless mode).${RESET}\n" + printf "\n" + printf " ${CYAN}Start the server:${RESET}\n" + printf " syncfu serve\n" + printf "\n" + printf " ${CYAN}Send a notification:${RESET}\n" + printf " syncfu send \"Hello from syncfu!\"\n" + printf "\n" + printf " For overlay notifications, install the desktop app:\n" + printf " curl -fsSL https://syncfu.dev/install.sh | sh\n" +fi +printf "\n" info "Done! Run ${BOLD}syncfu --help${RESET} to get started." printf "\n" diff --git a/package.json b/package.json index 3d6b7a0..a5b7e29 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "syncfu", "private": true, - "version": "0.2.0", + "version": "0.3.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2ae5394..2689d04 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "syncfu" -version = "0.2.0" +version = "0.3.0" edition = "2021" description = "Universal overlay notification system for AI agents and background processes" license = "MIT" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 344fbe3..99f8306 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicosujith/tauri-v2-schema/main/tauri.conf.json", "productName": "syncfu", - "version": "0.2.0", + "version": "0.3.0", "identifier": "dev.syncfu.app", "build": { "frontendDist": "../dist",