From b7ce929c50d18d250aeacae819d7ce52873092a7 Mon Sep 17 00:00:00 2001 From: "david.delagneau" Date: Wed, 20 May 2026 16:24:47 -0600 Subject: [PATCH] feat: add one-step installer script and enhance README with installation instructions --- apps/mac/AIWorkspace/README.md | 7 ++++ apps/mac/AIWorkspace/Sources/main.swift | 15 ++++++-- apps/mac/AIWorkspace/scripts/install.sh | 35 +++++++++++++++++ core/services/macos-installation-model.md | 47 +++++++++++++++++++++++ 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 apps/mac/AIWorkspace/scripts/install.sh create mode 100644 core/services/macos-installation-model.md diff --git a/apps/mac/AIWorkspace/README.md b/apps/mac/AIWorkspace/README.md index aad5a5f..f11d38c 100644 --- a/apps/mac/AIWorkspace/README.md +++ b/apps/mac/AIWorkspace/README.md @@ -28,6 +28,12 @@ Install to `~/Applications/AIWorkspace.app`: apps/mac/AIWorkspace/scripts/package-app.sh --install ``` +One-step local install, optionally enabling start at login and opening the app: + +```bash +apps/mac/AIWorkspace/scripts/install.sh --start-at-login --open +``` + ## Start at login After installing the app bundle: @@ -55,6 +61,7 @@ swift run --package-path apps/mac/AIWorkspace AIWorkspace - Stop Fidelity services - Restart Context MCP - Open Mattermost through the service manager +- Open Mattermost through the local proxy-managed launcher - Run Doctor - Copy Doctor JSON - Copy Photo Inbox URL diff --git a/apps/mac/AIWorkspace/Sources/main.swift b/apps/mac/AIWorkspace/Sources/main.swift index eee4e61..37bcf5e 100644 --- a/apps/mac/AIWorkspace/Sources/main.swift +++ b/apps/mac/AIWorkspace/Sources/main.swift @@ -203,7 +203,7 @@ struct ServiceMenuView: View { ActionButton(title: "Start Fidelity", systemImage: "play.fill", action: model.startProfile) ActionButton(title: "Stop Fidelity", systemImage: "stop.fill", role: .destructive, action: model.stopProfile) ActionButton(title: "Restart MCP", systemImage: "arrow.triangle.2.circlepath", action: model.restartMCP) - ActionButton(title: "Mattermost", systemImage: "message", action: model.openMattermost) + ActionButton(title: "Mattermost via Proxy", systemImage: "message.badge", action: model.openMattermost) } Divider() @@ -266,7 +266,7 @@ struct ServiceRow: View { VStack(alignment: .leading, spacing: 2) { Text(service.displayName) .font(.body.weight(.medium)) - Text(service.health.detail) + Text(service.detail) .font(.caption2) .foregroundStyle(.secondary) .lineLimit(1) @@ -277,7 +277,7 @@ struct ServiceRow: View { .padding(.vertical, 7) .padding(.horizontal, 10) .background(.quaternary.opacity(0.6), in: RoundedRectangle(cornerRadius: 10, style: .continuous)) - .help(service.health.detail) + .help(service.detail) } private var symbol: String { @@ -402,12 +402,19 @@ struct ServiceStatus: Decodable, Identifiable { switch name { case "aiw-context-mcp": "Context MCP" case "mattermost-proxy": "Mattermost Proxy" - case "mattermost-desktop": "Mattermost Desktop" + case "mattermost-desktop": "Mattermost Desktop via Proxy" case "photo-inbox": "Photo Inbox" default: name } } + var detail: String { + if name == "mattermost-desktop" { + return "Launches Mattermost through local proxy 127.0.0.1:8080" + } + return health.detail + } + var compactStatus: String { switch status { case "externally running": "external" diff --git a/apps/mac/AIWorkspace/scripts/install.sh b/apps/mac/AIWorkspace/scripts/install.sh new file mode 100644 index 0000000..6aabdd2 --- /dev/null +++ b/apps/mac/AIWorkspace/scripts/install.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +INSTALL_LOGIN_ITEM=0 +OPEN_APP=0 + +for arg in "$@"; do + case "$arg" in + --start-at-login) + INSTALL_LOGIN_ITEM=1 + ;; + --open) + OPEN_APP=1 + ;; + *) + echo "Unknown argument: $arg" >&2 + echo "Usage: $0 [--start-at-login] [--open]" >&2 + exit 1 + ;; + esac +done + +"$SCRIPT_DIR/package-app.sh" --install + +if [[ "$INSTALL_LOGIN_ITEM" == "1" ]]; then + "$SCRIPT_DIR/install-start-at-login.sh" +fi + +if [[ "$OPEN_APP" == "1" ]]; then + open "$HOME/Applications/AIWorkspace.app" +fi + +echo "AI Workspace app install complete." diff --git a/core/services/macos-installation-model.md b/core/services/macos-installation-model.md new file mode 100644 index 0000000..fcd5ff5 --- /dev/null +++ b/core/services/macos-installation-model.md @@ -0,0 +1,47 @@ +# macOS Installation Model + +## How production macOS utilities commonly do it + +Apps such as Cloudflare WARP, VPN clients, Docker Desktop, and device agents usually separate: + +- a user-facing app or menu bar app; +- one or more background services; +- launchd configuration for automatic startup; +- privileged helpers only when system-level networking, drivers, packet filtering, or protected paths are required. + +Common mechanisms: + +- `LaunchAgent` in `~/Library/LaunchAgents` for per-user background/login startup. +- `LaunchDaemon` in `/Library/LaunchDaemons` for root/system services. +- `SMAppService` / login items for sandboxed or App Store-aligned apps. +- Privileged helper tools via `SMJobBless` when admin-level installation is required. +- `.pkg` installers when the install needs privileged locations, daemons, receipts, or managed deployment. + +## Recommended AI Workspace approach + +Use a staged model: + +1. **Current local developer install** + - Build a real `.app` bundle into `apps/mac/AIWorkspace/dist/`. + - Install to `~/Applications/AIWorkspace.app`. + - Install a per-user `LaunchAgent` for start at login. + +2. **Production-ready local install** + - Keep using a per-user LaunchAgent because services are local user tools and do not require root. + - Add a one-step installer script that builds, installs, optionally enables start at login, and opens the app. + - Avoid privileged helpers until a real system-level requirement appears. + +3. **Future polished distribution** + - Create a signed/notarized `.app` or `.pkg`. + - Consider `SMAppService` for login item management from inside the app. + - Add a small daemon API if the UI needs richer lifecycle control than shelling out to `services.py`. + +## Why not LaunchDaemon now + +The current services are user-context services: + +- Mattermost Desktop launching must happen in the user's GUI session. +- Photo Inbox writes to user-owned folders and uses clipboard/notifications. +- The MCP and proxy bind localhost ports and do not require root. + +A root daemon would add unnecessary permission prompts and security risk. A per-user LaunchAgent is the correct production-leaning step for this stage.