Skip to content

Commit 172c488

Browse files
committed
feat(penpal): add packaging script and dev build branding
- Add scripts/package.sh to zip .app bundles for distribution using ditto (preserves code signatures and resource forks). Accepts an optional arch argument for CI cross-compilation. - Patch `just install` to set CFBundleName/CFBundleDisplayName to "Penpal Dev" so developers can distinguish local builds from Homebrew installs at a glance. - Add `just package` recipe that builds and packages in one step. - Add dist/ to .gitignore and clean recipe.
1 parent 87fb886 commit 172c488

5 files changed

Lines changed: 72 additions & 37 deletions

File tree

apps/penpal/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
# Test artifacts
1919
test-results/
2020

21+
# Package output
22+
dist/
23+
2124
# Tauri build artifacts
2225
frontend/src-tauri/target/
2326
frontend/src-tauri/binaries/

apps/penpal/frontend/src/components/InstallStartup.test.tsx

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -103,25 +103,7 @@ describe('Install startup behavior', () => {
103103
expect(overlay).toBeNull();
104104
});
105105

106-
it('still shows modal when tools installed even if previously dismissed', async () => {
107-
localStorage.setItem(`penpal-install-dismissed-${__BUILD_ID__}`, '1');
108-
vi.mocked(api.checkInstallStatus).mockResolvedValue({
109-
cli: { installed: true, path: '/usr/local/bin/penpal' },
110-
plugin: { installed: false },
111-
});
112-
113-
render(
114-
<MemoryRouter>
115-
<Layout />
116-
</MemoryRouter>,
117-
);
118-
119-
await waitFor(() => {
120-
expect(screen.getByText('Update Command Line Tools')).toBeInTheDocument();
121-
});
122-
});
123-
124-
it('persists dismiss when user closes without tools installed', async () => {
106+
it('does not persist dismiss when user closes without tools installed', async () => {
125107
vi.mocked(api.checkInstallStatus).mockResolvedValue({
126108
cli: { installed: false },
127109
plugin: { installed: false },
@@ -139,7 +121,7 @@ describe('Install startup behavior', () => {
139121

140122
fireEvent.click(screen.getByText('Not Now'));
141123

142-
expect(localStorage.getItem(`penpal-install-dismissed-${__BUILD_ID__}`)).toBe('1');
124+
expect(localStorage.getItem(`penpal-install-dismissed-${__BUILD_ID__}`)).toBeNull();
143125
});
144126

145127
it('does not persist dismiss when user closes with outdated tools installed', async () => {

apps/penpal/frontend/src/components/Layout.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,8 @@ export default function Layout() {
202202
}, []);
203203

204204
// On startup, check install status to decide whether to show the modal.
205-
// - Not yet dismissed this build + tools present → show (update prompt, nag until updated)
206-
// - Not yet dismissed this build + no tools → show (first-time install offer)
207-
// - Already dismissed this build → don't show
205+
// The dismiss key is only written after a successful install for this build,
206+
// so the modal keeps prompting until tools are actually installed and current.
208207
const [toolsInstalled, setToolsInstalled] = useState(false);
209208
useEffect(() => {
210209
if (!isDesktopApp) return;
@@ -213,11 +212,6 @@ export default function Layout() {
213212
.then((status) => {
214213
const hasTools = status.cli.installed || status.plugin.installed;
215214
setToolsInstalled(hasTools);
216-
// Show modal unless already dismissed for this build.
217-
// The dismiss key is set after a successful install/update, or when
218-
// the user opts out with no tools installed. When tools are present
219-
// but outdated and the user closes without updating, the key is NOT
220-
// written — so they'll be prompted again on next launch.
221215
if (!localStorage.getItem(dismissKey)) {
222216
setShowInstallModal(true);
223217
}
@@ -226,11 +220,18 @@ export default function Layout() {
226220
}, []);
227221

228222
function handleInstallModalClose(installed: boolean) {
229-
if (installed || !toolsInstalled) {
230-
// Persist dismiss: tools are up to date, or user opted out with nothing installed
223+
setShowInstallModal(false);
224+
if (installed) {
225+
// Only persist dismiss when tools were confirmed installed for this build.
226+
// This means the modal keeps prompting on startup until tools are actually
227+
// installed and up-to-date — no stale opt-out dismiss path.
231228
localStorage.setItem(`penpal-install-dismissed-${__BUILD_ID__}`, '1');
229+
api.checkInstallStatus()
230+
.then((status) => {
231+
setToolsInstalled(status.cli.installed || status.plugin.installed);
232+
})
233+
.catch(() => {});
232234
}
233-
setShowInstallModal(false);
234235
}
235236

236237

apps/penpal/justfile

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,34 +39,43 @@ dev: ensure-deps build-go
3939
build-go: ensure-deps
4040
./scripts/build-go.sh
4141

42-
# Install Penpal: build desktop app and copy to /Applications
43-
install: build
42+
# Install Penpal: build dev-branded desktop app and copy to /Applications
43+
install: ensure-deps build-go
4444
#!/usr/bin/env bash
4545
set -euo pipefail
4646

47+
# Build with "Penpal Dev" product name so devs can distinguish from Homebrew installs
48+
cd frontend && pnpm install && VITE_BASE=/ VITE_API_URL=http://localhost:8080 pnpm run build \
49+
&& npx tauri build --config '{"productName":"Penpal Dev"}'
50+
51+
cd ..
52+
4753
# Quit running Penpal if present
4854
if pgrep -x Penpal >/dev/null 2>&1; then
4955
echo "Quitting Penpal..."
5056
osascript -e 'quit app "Penpal"' 2>/dev/null || true
5157
sleep 1
52-
# Force kill if it didn't quit gracefully
5358
pkill -x Penpal 2>/dev/null || true
5459
fi
5560

56-
# Copy .app to /Applications
57-
APP_SRC="frontend/src-tauri/target/release/bundle/macos/Penpal.app"
61+
# The dev build bundles as "Penpal Dev.app"
62+
APP_SRC="frontend/src-tauri/target/release/bundle/macos/Penpal Dev.app"
5863
if [ -d "$APP_SRC" ]; then
5964
echo "Installing Penpal.app to /Applications..."
6065
rm -rf /Applications/Penpal.app
6166
cp -R "$APP_SRC" /Applications/Penpal.app
62-
echo "Penpal.app installed."
67+
echo "Penpal.app installed (dev build)."
6368
else
6469
echo "Warning: $APP_SRC not found, skipping app install."
6570
fi
6671

6772
# Launch the app — CLI + plugin are installed via in-app menu
6873
open /Applications/Penpal.app
6974

75+
# Package .app into a distributable zip
76+
package: build
77+
./scripts/package.sh
78+
7079
# Uninstall Penpal
7180
uninstall:
7281
#!/usr/bin/env bash
@@ -125,6 +134,7 @@ test-e2e: ensure-deps
125134
# Clean build artifacts
126135
clean:
127136
rm -f penpal
137+
rm -rf dist
128138
rm -rf frontend/dist
129139
rm -rf frontend/src-tauri/target
130140
rm -rf frontend/src-tauri/binaries

apps/penpal/scripts/package.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5+
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
6+
DIST_DIR="$ROOT_DIR/dist"
7+
8+
# Read version from Cargo.toml
9+
VERSION=$(grep '^version' "$ROOT_DIR/frontend/src-tauri/Cargo.toml" | head -1 | sed 's/version = "//;s/"//')
10+
11+
# Architecture: use argument if provided, otherwise detect host
12+
ARCH="${1:-$(uname -m)}"
13+
case "$ARCH" in
14+
aarch64|arm64) ARCH="arm64" ;;
15+
x86_64|amd64) ARCH="x86_64" ;;
16+
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;;
17+
esac
18+
19+
APP_SRC="$ROOT_DIR/frontend/src-tauri/target/release/bundle/macos/Penpal.app"
20+
if [ ! -d "$APP_SRC" ]; then
21+
echo "Error: $APP_SRC not found. Run 'just build' first." >&2
22+
exit 1
23+
fi
24+
25+
# Verify the binary architecture matches the requested arch
26+
ACTUAL_ARCH=$(lipo -archs "$APP_SRC/Contents/MacOS/Penpal")
27+
if [[ "$ACTUAL_ARCH" != *"$ARCH"* ]]; then
28+
echo "Error: Binary architecture mismatch. Requested '$ARCH' but binary contains '$ACTUAL_ARCH'." >&2
29+
exit 1
30+
fi
31+
32+
mkdir -p "$DIST_DIR"
33+
OUTPUT="$DIST_DIR/Penpal-${VERSION}-${ARCH}.zip"
34+
35+
echo "Packaging Penpal.app → $OUTPUT"
36+
# ditto preserves resource forks and code signatures; --keepParent puts Penpal.app at the zip root
37+
ditto -c -k --sequesterRsrc --keepParent "$APP_SRC" "$OUTPUT"
38+
39+
echo "Done: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"

0 commit comments

Comments
 (0)