···3232.build/
3333.derivedData/
34343535+# Embedded CLI binary (built by `make embed-cli`)
3636+Resources/prowl-cli/prowl
3737+3538# CocoaPods
3639#
3740# We recommend against adding the Pods directory to your .gitignore. However
+15-3
Makefile
···2020BUILD ?=
2121XCODEBUILD_FLAGS ?=
2222.DEFAULT_GOAL := help
2323-.PHONY: build-ghostty-xcframework ensure-ghostty sync-ghostty _check-ghostty-hash _record-ghostty-hash build-app build-cli run-app install-dev-build install-release archive export-archive format lint check test test-cli-smoke test-cli-integration bump-version bump-and-release log-stream
2323+.PHONY: build-ghostty-xcframework ensure-ghostty sync-ghostty _check-ghostty-hash _record-ghostty-hash build-app build-cli build-cli-release embed-cli run-app install-dev-build install-release archive export-archive format lint check test test-cli-smoke test-cli-integration bump-version bump-and-release log-stream
24242525help: # Display this help.
2626 @-+echo "Run make with one of the following targets:"
···8282 rm -rf ~/Library/Developer/Xcode/DerivedData/supacode-*
8383 @echo "Done. Xcode module cache cleared for fresh compilation."
84848585-build-app: ensure-ghostty # Build the macOS app (Debug)
8585+build-app: ensure-ghostty embed-cli # Build the macOS app (Debug)
8686 bash -o pipefail -c 'xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Debug build -skipMacroValidation -clonedSourcePackagesDirPath $(SPM_CACHE_DIR) 2>&1 | mise exec -- xcsift -qw --format toon'
87878888build-cli: # Build Swift CLI binary (SPM)
8989 swift build --product prowl
9090+9191+build-cli-release: # Build CLI binary in release mode
9292+ swift build -c release --product prowl
9393+9494+embed-cli: build-cli-release # Build CLI and copy into Resources for app bundling
9595+ @set -euo pipefail; \
9696+ bin="$$(swift build -c release --show-bin-path)/prowl"; \
9797+ dst="$(CURRENT_MAKEFILE_DIR)/Resources/prowl-cli"; \
9898+ mkdir -p "$$dst"; \
9999+ cp "$$bin" "$$dst/prowl"; \
100100+ chmod +x "$$dst/prowl"; \
101101+ echo "embedded CLI binary at $$dst/prowl"
9010291103run-app: build-app # Build then launch (Debug) with log streaming
92104 @set -euo pipefail; \
···234246 ditto "$$APP_PATH" "$$DST"; \
235247 echo "installed $$DST (Release build, locally signed)"
236248237237-archive: build-ghostty-xcframework # Archive Release build for distribution
249249+archive: build-ghostty-xcframework embed-cli # Archive Release build for distribution
238250 bash -o pipefail -c 'xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Release -archivePath build/supacode.xcarchive archive CODE_SIGN_STYLE=Manual DEVELOPMENT_TEAM="$$APPLE_TEAM_ID" CODE_SIGN_IDENTITY="$$DEVELOPER_ID_IDENTITY_SHA" OTHER_CODE_SIGN_FLAGS="--timestamp" -skipMacroValidation -clonedSourcePackagesDirPath $(SPM_CACHE_DIR) $(XCODEBUILD_FLAGS) 2>&1 | mise exec -- xcsift -qw --format toon'
239251240252export-archive: # Export xarchive
Resources/prowl-cli/.gitkeep
This is a binary file and will not be displayed.
+182
docs/plans/2026-04-04-cli-install-command.md
···11+# CLI Install Command Implementation Plan
22+33+**Goal:** Allow users to install the `prowl` CLI tool from within the Prowl app via three entry points: Settings, Prowl menu, and Command Palette.
44+55+**Scope:**
66+- In: CLIInstallClient dependency, Advanced Settings UI, Prowl menu item, Command Palette item, AppFeature wiring, Makefile CLI embedding, tests
77+- Out: Auto-prompting on first launch, uninstall UI (can be added later), CLI build as part of Xcode build phase (manual `make build-cli` for now)
88+99+**Architecture:**
1010+- `CLIInstallClient`: TCA dependency client that handles symlink creation, status checking, and bundled binary path resolution
1111+- Install action lives in `AppFeature` — all three entry points (Settings, Menu, Command Palette) funnel into the same `installCLI` action
1212+- CLI binary is embedded at `Prowl.app/Contents/Resources/prowl-cli/prowl`
1313+- Installation creates a symlink: `/usr/local/bin/prowl` → bundled binary path
1414+- Advanced Settings gets a new "Command Line Tool" section showing install status + install/uninstall button
1515+1616+**Acceptance / Verification:**
1717+- `make build-app` succeeds
1818+- All existing tests pass
1919+- New CLIInstallClient tests pass
2020+- New AppFeature CLI install reducer tests pass
2121+- Menu item "Install Command Line Tool" visible under Prowl menu
2222+- Command Palette shows "Install Command Line Tool" item
2323+- Settings > Advanced shows CLI install section with status and action button
2424+2525+---
2626+2727+## Task 1: Create CLIInstallClient dependency
2828+2929+**Files:**
3030+- Create: `supacode/Clients/CLIInstall/CLIInstallClient.swift`
3131+3232+**Steps:**
3333+1. Create `CLIInstallClient` struct following `WorkspaceClient` pattern
3434+2. Provide operations:
3535+ - `bundledCLIURL: @Sendable () -> URL?` — returns `Bundle.main.resourceURL/prowl-cli/prowl`
3636+ - `installationStatus: @Sendable () -> CLIInstallStatus` — checks if symlink exists and points to correct target
3737+ - `install: @Sendable (URL) async throws -> Void` — creates symlink at given path (default `/usr/local/bin/prowl`)
3838+ - `uninstall: @Sendable (URL) async throws -> Void` — removes symlink at given path
3939+3. Define `CLIInstallStatus` enum: `.notInstalled`, `.installed(path: String)`, `.installedDifferentSource(path: String)`
4040+4. Implement `DependencyKey` with `liveValue` and `testValue`
4141+5. Register in `DependencyValues`
4242+4343+**Notes:**
4444+- Use `FileManager` for symlink operations
4545+- `install` should create `/usr/local/bin` directory if it doesn't exist
4646+- Check if destination already exists before creating symlink; if it's a symlink pointing elsewhere, report `.installedDifferentSource`
4747+4848+---
4949+5050+## Task 2: Add CLI install actions to AppFeature
5151+5252+**Files:**
5353+- Modify: `supacode/Features/App/Reducer/AppFeature.swift` (add actions and reducer cases)
5454+5555+**Steps:**
5656+1. Add new actions to AppFeature.Action:
5757+ - `installCLI`
5858+ - `uninstallCLI`
5959+ - `cliInstallResult(Result<String, CLIInstallError>)` — result of install/uninstall with success message or error
6060+2. Add `@Dependency(CLIInstallClient.self)` to AppFeature
6161+3. Implement reducer cases:
6262+ - `installCLI`: run `.install()` via client, send result action
6363+ - `uninstallCLI`: run `.uninstall()` via client, send result action
6464+ - `cliInstallResult`: show alert with success/failure message
6565+4. Add `CLIInstallError` type for error reporting
6666+6767+---
6868+6969+## Task 3: Add CLI install section to Advanced Settings
7070+7171+**Files:**
7272+- Modify: `supacode/Features/Settings/Views/AdvancedSettingsView.swift` (add CLI section)
7373+7474+**Steps:**
7575+1. Add a new `Section("Command Line Tool")` in `AdvancedSettingsView`
7676+2. Show current installation status (use `CLIInstallClient` to check)
7777+3. Show Install/Uninstall button based on status
7878+4. Button sends action to the `AppFeature` store (the settings view already receives `StoreOf<SettingsFeature>`, but we need to access `AppFeature` actions — use a callback or add delegate actions)
7979+8080+**Design decision:** Since AdvancedSettingsView only has `StoreOf<SettingsFeature>`, add delegate actions to SettingsFeature:
8181+- `SettingsFeature.Delegate.installCLIRequested`
8282+- `SettingsFeature.Delegate.uninstallCLIRequested`
8383+- Handle these in AppFeature's `.settings(.delegate(...))` case
8484+8585+**Notes:**
8686+- Show the install path (`/usr/local/bin/prowl`) in the UI
8787+- Show a green checkmark or status text for installed state
8888+- The view should refresh status when the settings tab appears
8989+9090+---
9191+9292+## Task 4: Add menu item in Prowl menu
9393+9494+**Files:**
9595+- Modify: `supacode/App/supacodeApp.swift` (add menu item in Prowl menu group)
9696+9797+**Steps:**
9898+1. Add a `CommandGroup(after: .appSettings)` or within the existing Prowl menu area
9999+2. Add "Install Command Line Tool..." button
100100+3. Button sends `store.send(.installCLI)` action
101101+4. Add appropriate `.help()` text
102102+103103+---
104104+105105+## Task 5: Add Command Palette item
106106+107107+**Files:**
108108+- Modify: `supacode/Features/CommandPalette/CommandPaletteItem.swift` (add Kind case)
109109+- Modify: `supacode/Features/CommandPalette/Reducer/CommandPaletteFeature.swift` (add item, delegate, mapping)
110110+- Modify: `supacode/Features/App/Reducer/AppFeature.swift` (handle new delegate)
111111+112112+**Steps:**
113113+1. Add `case installCLI` to `CommandPaletteItem.Kind`
114114+2. Update `isGlobal` and `isRootAction` to return `true` for `.installCLI`
115115+3. Add `case installCLI` to `CommandPaletteFeature.Delegate`
116116+4. Add `CommandPaletteItem` to `commandPaletteItems()` function
117117+5. Add ID `globalInstallCLI` to `CommandPaletteItemID`
118118+6. Add to `globalIDs` array
119119+7. Update `delegateAction(for:)` mapping
120120+8. Update `appShortcutCommandID` (return nil for installCLI)
121121+9. Handle `.commandPalette(.delegate(.installCLI))` in AppFeature reducer
122122+123123+---
124124+125125+## Task 6: Makefile integration for CLI embedding
126126+127127+**Files:**
128128+- Modify: `Makefile` (add target to build CLI for bundle)
129129+130130+**Steps:**
131131+1. Add `build-cli-release` target: `swift build -c release --product prowl`
132132+2. Add `embed-cli` target: copies release binary to `Resources/prowl-cli/prowl`
133133+3. Update `build-app` to depend on `embed-cli` (or document manual step)
134134+4. Add `Resources/prowl-cli/` to Xcode "Copy Bundle Resources" if not auto-included
135135+136136+**Notes:**
137137+- For development, `Resources/prowl-cli/prowl` can be a placeholder — the actual install will use the bundled path at runtime
138138+139139+---
140140+141141+## Task 7: Tests for CLIInstallClient
142142+143143+**Files:**
144144+- Create: `supacodeTests/CLIInstallClientTests.swift`
145145+146146+**Steps:**
147147+1. Test `installationStatus` returns `.notInstalled` when no symlink exists
148148+2. Test `installationStatus` returns `.installed` when valid symlink exists
149149+3. Test `installationStatus` returns `.installedDifferentSource` when symlink points elsewhere
150150+4. Test `install` creates symlink at expected path
151151+5. Test `install` creates parent directory if needed
152152+6. Test `uninstall` removes symlink
153153+7. Test `uninstall` does not remove non-symlink files (safety)
154154+155155+**Notes:**
156156+- Use temp directories for test isolation
157157+- Test with actual FileManager operations (not mocks) for the live client
158158+159159+---
160160+161161+## Task 8: Tests for AppFeature CLI install reducer
162162+163163+**Files:**
164164+- Create: `supacodeTests/AppFeatureCLIInstallTests.swift`
165165+166166+**Steps:**
167167+1. Test `.installCLI` action triggers client install call
168168+2. Test `.uninstallCLI` action triggers client uninstall call
169169+3. Test success result shows appropriate alert
170170+4. Test failure result shows error alert
171171+5. Test Command Palette delegate `.installCLI` forwards to `.installCLI` action
172172+6. Test Settings delegate `.installCLIRequested` forwards to `.installCLI` action
173173+174174+---
175175+176176+## Task 9: Build verification
177177+178178+**Steps:**
179179+1. Run `make build-app` — verify success
180180+2. Run existing tests — verify no regressions
181181+3. Run new tests — verify all pass
182182+4. Run `make lint` — verify no lint errors