feat: add one-step installer script and enhance README with installation instructions
This commit is contained in:
@@ -28,6 +28,12 @@ Install to `~/Applications/AIWorkspace.app`:
|
|||||||
apps/mac/AIWorkspace/scripts/package-app.sh --install
|
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
|
## Start at login
|
||||||
|
|
||||||
After installing the app bundle:
|
After installing the app bundle:
|
||||||
@@ -55,6 +61,7 @@ swift run --package-path apps/mac/AIWorkspace AIWorkspace
|
|||||||
- Stop Fidelity services
|
- Stop Fidelity services
|
||||||
- Restart Context MCP
|
- Restart Context MCP
|
||||||
- Open Mattermost through the service manager
|
- Open Mattermost through the service manager
|
||||||
|
- Open Mattermost through the local proxy-managed launcher
|
||||||
- Run Doctor
|
- Run Doctor
|
||||||
- Copy Doctor JSON
|
- Copy Doctor JSON
|
||||||
- Copy Photo Inbox URL
|
- Copy Photo Inbox URL
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ struct ServiceMenuView: View {
|
|||||||
ActionButton(title: "Start Fidelity", systemImage: "play.fill", action: model.startProfile)
|
ActionButton(title: "Start Fidelity", systemImage: "play.fill", action: model.startProfile)
|
||||||
ActionButton(title: "Stop Fidelity", systemImage: "stop.fill", role: .destructive, action: model.stopProfile)
|
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: "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()
|
Divider()
|
||||||
@@ -266,7 +266,7 @@ struct ServiceRow: View {
|
|||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(service.displayName)
|
Text(service.displayName)
|
||||||
.font(.body.weight(.medium))
|
.font(.body.weight(.medium))
|
||||||
Text(service.health.detail)
|
Text(service.detail)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@@ -277,7 +277,7 @@ struct ServiceRow: View {
|
|||||||
.padding(.vertical, 7)
|
.padding(.vertical, 7)
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
.background(.quaternary.opacity(0.6), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
|
.background(.quaternary.opacity(0.6), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||||
.help(service.health.detail)
|
.help(service.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var symbol: String {
|
private var symbol: String {
|
||||||
@@ -402,12 +402,19 @@ struct ServiceStatus: Decodable, Identifiable {
|
|||||||
switch name {
|
switch name {
|
||||||
case "aiw-context-mcp": "Context MCP"
|
case "aiw-context-mcp": "Context MCP"
|
||||||
case "mattermost-proxy": "Mattermost Proxy"
|
case "mattermost-proxy": "Mattermost Proxy"
|
||||||
case "mattermost-desktop": "Mattermost Desktop"
|
case "mattermost-desktop": "Mattermost Desktop via Proxy"
|
||||||
case "photo-inbox": "Photo Inbox"
|
case "photo-inbox": "Photo Inbox"
|
||||||
default: name
|
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 {
|
var compactStatus: String {
|
||||||
switch status {
|
switch status {
|
||||||
case "externally running": "external"
|
case "externally running": "external"
|
||||||
|
|||||||
35
apps/mac/AIWorkspace/scripts/install.sh
Normal file
35
apps/mac/AIWorkspace/scripts/install.sh
Normal file
@@ -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."
|
||||||
47
core/services/macos-installation-model.md
Normal file
47
core/services/macos-installation-model.md
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user