Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-14 11:43:20 -06:00
parent ae787ca060
commit 4c1c6f31d5
5 changed files with 169 additions and 39 deletions

View File

@ -10,12 +10,13 @@ Deliver one installable macOS app that:
- launches an embedded backend executable from app resources
- opens the web UI only inside `WKWebView`
- persists settings locally and supports native-to-web sync
- provides native app-level help/onboarding (not only in the web UI)
- can be packaged as a DMG
## Layout Standard
Use this layout for new projects:
- `web/` for web backend source (`app.py`, modules, requirements)
- `mac/<AppName>Mac/` for Xcode project and SwiftUI shell
- `web/src/` for web backend source (`app.py`, modules, requirements)
- `mac/src/` for Xcode project and SwiftUI shell sources
- `scripts/` for build and packaging automation
- `docs/` for architecture and onboarding docs
@ -34,35 +35,52 @@ Detailed naming guidance: `references/layout-and-naming.md`
3. Build backend into a single executable.
- Add `scripts/build_embedded_backend.sh` that compiles backend into a one-file executable and copies it into the mac target resource folder.
- Use generic discovery:
- discover `*.xcodeproj` under `mac/src`
- discover scheme via `xcodebuild -list -json` (fallback to project name)
- discover/create `EmbeddedBackend` folder under selected project sources
- Include all required web files/modules in build inputs.
- Default backend name should be stable (for example `WebBackend`) and configurable via env.
4. Implement SwiftUI host shell.
- Use `@Observable` host state (no `ObservableObject`/Combine).
- Start/stop backend process, detect available local port, and handle retries.
- Render URL in `WKWebView` only.
- Keep app responsive when backend fails and surface actionable status.
- Resolve backend executable by checking both current and legacy names during migration.
- Add a native toolbar `Help` button that opens bundled help content in-app.
5. Define settings sync contract.
- Use shared settings file (for example `~/.<app>/settings.json`) as source of truth.
- Normalize settings both in web app and native app.
- Pass effective settings via URL query params on launch/reload/restart.
- Keep onboarding limited to starter fields; preserve advanced fields.
- Add web-only fallback access to help/onboarding (for users not using the mac wrapper).
6. Automate packaging.
- Add `scripts/build_selfcontained_mac_app.sh` to build embedded backend then Xcode app.
- Add `scripts/create_installer_dmg.sh` for distributable DMG.
- Use generic app bundle discovery for DMG defaults where possible.
- Standardize installer artifacts under `build/dmg/` (not repo root).
- Ensure `.gitignore` excludes generated artifacts (`build/`, `*.dmg`, temp `rw.*.dmg`).
7. Validate.
- Python syntax: `python -m py_compile` on web entrypoint.
- Tests: `PYTHONPATH=web/src pytest -q web/src/tests`.
- Xcode build: `xcodebuild ... build`.
- Runtime check: no external browser opens, webview loads locally, settings persist across relaunch.
- Verify help works in both modes:
- native toolbar help popup in mac app
- sidebar help fallback in web-only mode
## Required Deliverables
- Embedded backend build script
- macOS app host that launches backend + WKWebView
- Shared settings sync path
- Native help/onboarding popup + web fallback help entry
- README section for local build + DMG workflow
- Verified build commands and final artifact locations
- `.gitignore` updated for build/installer outputs
## References
- Layout and migration rules: `references/layout-and-naming.md`

View File

@ -1,4 +1,4 @@
interface:
display_name: "macOS Self-Contained Web App"
short_description: "Embed web backend in local Mac app."
default_prompt: "Create or refactor this project into a self-contained macOS app wrapper for a local web backend running inside WKWebView."
short_description: "Embed a local web backend in a self-contained macOS WKWebView app."
default_prompt: "Create or refactor this project into a self-contained macOS app wrapper with web/src + mac/src layout, embedded backend binary, native Help popup, DMG output in build/dmg, and proper .gitignore rules."

View File

@ -7,30 +7,41 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WEB_SRC_DIR="$ROOT_DIR/web/src"
MAC_SRC_DIR="${MAC_SRC_DIR:-$ROOT_DIR/mac/src}"
PYTHON_BIN="$ROOT_DIR/.venv/bin/python"
APP_NAME="TraderBackend"
BACKEND_BIN_NAME="${BACKEND_BIN_NAME:-WebBackend}"
BUILD_ROOT="$ROOT_DIR/dist-backend-build"
DIST_PATH="$BUILD_ROOT/dist"
WORK_PATH="$BUILD_ROOT/build"
SPEC_PATH="$BUILD_ROOT/spec"
TARGET_DIR="$ROOT_DIR/mac/TraderMac/TraderMac/EmbeddedBackend"
PROJECT_PATH="${MAC_PROJECT_PATH:-}"
SCHEME="${MAC_SCHEME:-}"
TARGET_DIR="${EMBEDDED_BACKEND_DIR:-}"
if [[ -z "$PROJECT_PATH" ]]; then
PROJECT_PATH="$(find "$MAC_SRC_DIR" -maxdepth 4 -name "*.xcodeproj" | sort | head -n 1)"
fi
PROJECT_DIR="$(dirname "$PROJECT_PATH")"
if [[ -z "$TARGET_DIR" ]]; then
TARGET_DIR="$(find "$PROJECT_DIR" -maxdepth 4 -type d -name EmbeddedBackend | head -n 1)"
fi
mkdir -p "$DIST_PATH" "$WORK_PATH" "$SPEC_PATH" "$TARGET_DIR"
"$PYTHON_BIN" -m pip install -q pyinstaller
"$PYTHON_BIN" -m PyInstaller \
--noconfirm --clean --onefile \
--name "$APP_NAME" \
--name "$BACKEND_BIN_NAME" \
--distpath "$DIST_PATH" \
--workpath "$WORK_PATH" \
--specpath "$SPEC_PATH" \
--add-data "$ROOT_DIR/web/app.py:." \
--add-data "$ROOT_DIR/web/<module_dir>:<module_dir>" \
"$ROOT_DIR/web/backend_embedded_launcher.py"
--add-data "$WEB_SRC_DIR/app.py:." \
--add-data "$WEB_SRC_DIR/<module_dir>:<module_dir>" \
"$WEB_SRC_DIR/backend_embedded_launcher.py"
cp "$DIST_PATH/$APP_NAME" "$TARGET_DIR/$APP_NAME"
chmod +x "$TARGET_DIR/$APP_NAME"
cp "$DIST_PATH/$BACKEND_BIN_NAME" "$TARGET_DIR/$BACKEND_BIN_NAME"
chmod +x "$TARGET_DIR/$BACKEND_BIN_NAME"
```
## SwiftUI Host Requirements
@ -40,6 +51,7 @@ chmod +x "$TARGET_DIR/$APP_NAME"
- Render backend URL in `WKWebView`.
- Retry provisional load failure after short delay.
- Keep debug controls behind `#if DEBUG`.
- Add toolbar Help action that opens a bundled local `help.html` in a sheet/webview.
## Settings Sync Contract
- Shared path: `~/.<app>/settings.json`
@ -47,6 +59,7 @@ chmod +x "$TARGET_DIR/$APP_NAME"
- Load shared file before app launch.
- Push normalized fields as query params when launching/reloading webview.
- Persist setup-sheet changes back to shared file.
- Keep a legacy fallback path for migration when needed.
## Packaging Commands
- Build app:
@ -61,9 +74,26 @@ chmod +x "$TARGET_DIR/$APP_NAME"
APP_BUNDLE_PATH="dist-mac/<timestamp>/<AppName>Mac.app" ./scripts/create_installer_dmg.sh
```
Expected DMG output:
```text
build/dmg/<AppName>-<timestamp>.dmg
```
## Git Ignore Baseline
```gitignore
build/
*.dmg
rw.*.dmg
```
## Verification Checklist
- No external browser window opens.
- App starts with embedded backend from app resources.
- `WKWebView` loads local URL.
- Settings persist across relaunch and remain in sync.
- Native Help popup renders bundled content.
- Web-only run has sidebar help fallback.
- DMG installs and runs on a second machine.
- DMG is produced under `build/dmg/` and repo root stays clean.

View File

@ -5,44 +5,68 @@
```text
<repo-root>/
web/
run.sh
src/
app.py
<web modules>
requirements.txt
ONBOARDING.md
mac/
<AppName>Mac/
<AppName>Mac.xcodeproj/
<AppName>Mac/
src/
<Project>.xcodeproj/
App/
ContentView.swift
<AppName>MacApp.swift
<AppMain>.swift
EmbeddedBackend/
<AppName>Backend
WebBackend
Help/
help.html
AppTests/
AppUITests/
scripts/
build_embedded_backend.sh
build_selfcontained_mac_app.sh
create_installer_dmg.sh
build/
dmg/
<AppName>-<timestamp>.dmg
docs/
architecture.md
onboarding.md
```
## Naming Rules
- Repo: lowercase hyphenated (`trader-desktop-shell`).
- macOS app target/project: PascalCase with `Mac` suffix (`TraderMac`).
- Embedded backend binary: PascalCase + `Backend` (`TraderBackend`).
- macOS target/scheme: PascalCase (`TraderMac` or project-defined), but source folders should stay generic (`App`, `AppTests`, `AppUITests`).
- Embedded backend binary: stable generic default (`WebBackend`) with env override support.
- Settings directory: lowercase snake or kebab (`~/.trader_app`).
- Environment port var: uppercase snake (`TRADER_PORT`).
- Environment port var: uppercase snake (keep legacy fallbacks during migration).
## Script Discovery Rules
- Build scripts should discover the first `*.xcodeproj` under `mac/src` unless `MAC_PROJECT_PATH` is provided.
- Build scripts should discover scheme via `xcodebuild -list -json` unless `MAC_SCHEME` is provided.
- Embedded backend target dir should be derived from selected project sources or `EMBEDDED_BACKEND_DIR`.
- Python tests should run with `PYTHONPATH=web/src`.
- DMG scripts should write to `build/dmg/` and clean temporary staging folders.
## Ignore Rules
Add these to root `.gitignore`:
- `build/`
- `*.dmg`
- `rw.*.dmg`
## Migration Rules For Existing Projects
1. Do not rename everything in one commit.
2. First, add new folders and compatibility references.
3. Move scripts and docs next.
4. Move web source only after build scripts are updated.
5. Rename Xcode project/target last, with a dedicated verification commit.
5. Move mac source into `mac/src/App*` before optional target/project renames.
6. Add compatibility lookup for old backend binary names and old settings paths during transition.
## Commit Strategy
1. `chore(layout): add canonical folders`
2. `build(backend): add embedded binary build`
3. `feat(mac-shell): host local backend in webview`
4. `feat(sync): add shared settings contract`
5. `build(packaging): add self-contained app + dmg scripts`
6. `chore(rename): finalize naming migration`
5. `feat(help): add native help popup + web fallback`
6. `build(packaging): add self-contained app + dmg scripts`
7. `chore(rename): finalize naming migration`

View File

@ -1,28 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: $0 <repo-root> <app-name-pascal>"
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <repo-root> [app-name]"
echo "Example: $0 ~/Code/trader-app Trader"
exit 1
fi
REPO_ROOT="$1"
APP_NAME="$2"
MAC_APP_NAME="${APP_NAME}Mac"
BACKEND_NAME="${APP_NAME}Backend"
APP_NAME="${2:-WebShellApp}"
mkdir -p "$REPO_ROOT/web"
mkdir -p "$REPO_ROOT/mac/$MAC_APP_NAME/$MAC_APP_NAME/EmbeddedBackend"
mkdir -p "$REPO_ROOT/web/src"
mkdir -p "$REPO_ROOT/web/src/web_core"
mkdir -p "$REPO_ROOT/web/src/tests"
mkdir -p "$REPO_ROOT/mac/src/App/EmbeddedBackend"
mkdir -p "$REPO_ROOT/mac/src/App/Help"
mkdir -p "$REPO_ROOT/mac/src/AppTests"
mkdir -p "$REPO_ROOT/mac/src/AppUITests"
mkdir -p "$REPO_ROOT/scripts"
mkdir -p "$REPO_ROOT/docs"
cat > "$REPO_ROOT/docs/architecture.md" <<DOC
# ${APP_NAME} Architecture
- web backend source: ./web
- mac shell source: ./mac/${MAC_APP_NAME}
- embedded backend binary: ./mac/${MAC_APP_NAME}/${MAC_APP_NAME}/EmbeddedBackend/${BACKEND_NAME}
- web backend source: ./web/src
- mac shell source: ./mac/src
- embedded backend binary: ./mac/src/App/EmbeddedBackend/WebBackend
- native help page: ./mac/src/App/Help/help.html
DOC
cat > "$REPO_ROOT/scripts/README.build.md" <<DOC
@ -32,10 +36,64 @@ Add:
- build_embedded_backend.sh
- build_selfcontained_mac_app.sh
- create_installer_dmg.sh
Suggested script behaviors:
- Discover \`*.xcodeproj\` under \`mac/src\` unless \`MAC_PROJECT_PATH\` is provided.
- Discover scheme via \`xcodebuild -list -json\` unless \`MAC_SCHEME\` is provided.
- Default backend binary name: \`WebBackend\` (override with \`BACKEND_BIN_NAME\`).
- Write DMG artifacts to \`build/dmg/\`.
DOC
cat > "$REPO_ROOT/web/src/ONBOARDING.md" <<DOC
# ${APP_NAME} Onboarding
Add your web help/onboarding content here.
DOC
cat > "$REPO_ROOT/mac/src/App/Help/help.html" <<DOC
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Help</title>
</head>
<body>
<h1>${APP_NAME} Help</h1>
<p>Replace this with your quick start and onboarding content.</p>
</body>
</html>
DOC
cat > "$REPO_ROOT/web/run.sh" <<'DOC'
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VENV_DIR="$ROOT_DIR/.venv"
WEB_SRC_DIR="$ROOT_DIR/web/src"
if [[ ! -d "$VENV_DIR" ]]; then
python3 -m venv "$VENV_DIR"
fi
# shellcheck disable=SC1091
source "$VENV_DIR/bin/activate"
pip install -r "$WEB_SRC_DIR/requirements.txt"
exec streamlit run "$WEB_SRC_DIR/app.py"
DOC
chmod +x "$REPO_ROOT/web/run.sh"
touch "$REPO_ROOT/.gitignore"
for line in "build/" "*.dmg" "rw.*.dmg"; do
if ! grep -Fxq "$line" "$REPO_ROOT/.gitignore"; then
printf "%s\n" "$line" >> "$REPO_ROOT/.gitignore"
fi
done
echo "Created layout for ${APP_NAME}:"
echo "- $REPO_ROOT/web"
echo "- $REPO_ROOT/mac/$MAC_APP_NAME"
echo "- $REPO_ROOT/web/src"
echo "- $REPO_ROOT/mac/src"
echo "- $REPO_ROOT/scripts"
echo "- $REPO_ROOT/docs"
echo "- $REPO_ROOT/.gitignore (updated with build/DMG ignore rules)"