Files
gui-video-clipper/docs/superpowers/plans/2026-09-22-build-cleanup-release-packaging.md
cottongin d46973a719 v0.1.1: fix .app bundle PATH for pyenv/nvm, detect yt-dlp pot-provider plugin
- fix_path_env() now adds ~/.pyenv/shims (with PYENV_ROOT) before
  Homebrew paths so pyenv-managed yt-dlp (with pip-installed plugins)
  takes priority over Homebrew's bare yt-dlp binary
- Also handles ~/.nvm, ~/.deno/bin, ~/.cargo/bin for broader coverage
- check_pot_plugin uses lightweight 'yt-dlp -v' instead of network
  simulation for reliable plugin detection
- Bump version to 0.1.1

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 14:09:23 -04:00

734 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Build Cleanup & Release Packaging Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Eliminate all build warnings/errors across frontend and backend, fix the failing test, and implement a release build script that produces a distributable `GUI Video Clipper.app`.
**Architecture:** Direct fixes to existing files — no new components or modules except `scripts/build-release.sh`. TypeScript config fixes, Svelte a11y attribute additions, Rust `#[allow]` annotations, one test assertion update, and a new shell script.
**Tech Stack:** Svelte 5, TypeScript, Vite/Vitest, Rust/Cargo, Tauri v2, Bash
## Global Constraints
- macOS is the only target platform
- All changes must result in zero warnings from `npm run check`, `cargo build`, and `npm test`
- Preserve existing runtime behavior — these are lint/warning fixes, not behavioral changes
- Follow existing code patterns and styles in each file
---
### Task 1: TypeScript Error Fixes
**Files:**
- Modify: `tsconfig.json`
- Modify: `vite.config.ts:1` (import line only)
- Test: `npm run check` (svelte-check)
**Interfaces:**
- Consumes: nothing
- Produces: Clean `npm run check` (0 errors for T1–T4; a11y warnings will still be present until Task 2)
- [ ] **Step 1: Install `@types/node`**
Run:
```bash
npm install -D @types/node
```
Expected: Package added to `devDependencies` in `package.json`.
- [ ] **Step 2: Add `"types": ["node"]` to `tsconfig.json`**
Edit `tsconfig.json` — add `"types": ["node"]` inside `compilerOptions`:
```json
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler",
"module": "ESNext",
"target": "ESNext",
"isolatedModules": true,
"types": ["node"],
"paths": {
"$lib/*": ["./src/lib/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.svelte", "tests/**/*.ts", "vite.config.ts"]
}
```
- [ ] **Step 3: Change `defineConfig` import in `vite.config.ts`**
Change line 4 from:
```typescript
import { defineConfig } from 'vite';
```
to:
```typescript
import { defineConfig } from 'vitest/config';
```
This re-export extends `UserConfigExport` with vitest's `test` property.
- [ ] **Step 4: Verify TS errors are resolved**
Run:
```bash
npm run check 2>&1 | grep -E "^(Error|.*Error:)"
```
Expected: No lines matching "Error" — only a11y warnings remain.
- [ ] **Step 5: Commit**
```bash
git add tsconfig.json vite.config.ts package.json package-lock.json
git commit -m "fix: resolve TypeScript errors in vite.config.ts
- Install @types/node for node:path, node:process, node:url
- Import defineConfig from vitest/config for test config type
- Add types: [\"node\"] to tsconfig.json"
```
---
### Task 2: Svelte A11y Warning Fixes
**Files:**
- Modify: `src/lib/components/CaptionSettingsPanel.svelte:48-209` (label associations)
- Modify: `src/lib/components/ClipList.svelte:46` (tabindex + keydown)
- Modify: `src/lib/components/PreferencesPanel.svelte:60-61` (tabindex + keydown)
- Modify: `src/lib/components/ExportDialog.svelte:139-140` (tabindex + keydown)
- Modify: `src/lib/components/AboutDialog.svelte:15-16` (tabindex + keydown)
- Modify: `src/lib/components/VideoPlayer.svelte:187-197` (track element)
- Modify: `src/App.svelte:188-193` (resize handle keyboard support)
- Test: `npm run check` (svelte-check)
**Interfaces:**
- Consumes: Task 1 completed (TS errors resolved)
- Produces: Zero a11y warnings from `npm run check` and `npm run build`
- [ ] **Step 1: Fix CaptionSettingsPanel labels (9 warnings)**
The labels at lines 48, 61, 74, 90, 108, 120, 165, 195, and 209 are not associated with their controls. Fix each by adding `id` attributes to controls and `for` attributes to labels. Use unique IDs prefixed with `caption-`.
**Line 48 — Font label + select:** Change:
```svelte
<label>Font</label>
<select
value={settings.fontFamily}
```
to:
```svelte
<label for="caption-font">Font</label>
<select
id="caption-font"
value={settings.fontFamily}
```
**Line 61 — Font Size label + range input:** The label wraps the text but not the input. Wrap the input inside the label:
```svelte
<label>
Font Size
<span class="value">{settings.fontSize}px</span>
<input
type="range" min="12" max="36" step="1"
value={settings.fontSize}
oninput={(e) => update({ fontSize: parseInt((e.target as HTMLInputElement).value, 10) })}
/>
</label>
```
Remove the standalone `<input>` that was a sibling below the closing `</label>`.
**Line 74 — Text Color label:** Change:
```svelte
<label>Text Color</label>
<div class="row-controls">
<input
type="color" value={settings.textColor}
```
to:
```svelte
<label for="caption-text-color">Text Color</label>
<div class="row-controls">
<input
id="caption-text-color"
type="color" value={settings.textColor}
```
**Line 90 — Dimmed Text label:** This is a heading-style label for a radio group. Use `id`/`for` pointing to the first radio:
```svelte
<label for="caption-dimmed-auto">Dimmed Text</label>
<div class="radio-row">
<label>
<input id="caption-dimmed-auto" type="radio" name="dimmed-mode" value="auto"
```
**Line 108 — Dim Opacity label + range:** Wrap the input inside the label (same pattern as Font Size):
```svelte
<label>
Dim Opacity
<span class="value">{Math.round(settings.dimmedOpacity * 100)}%</span>
<input
type="range" min="10" max="90" step="5"
value={Math.round(settings.dimmedOpacity * 100)}
oninput={(e) => update({ dimmedOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
/>
</label>
```
Remove the standalone `<input>` below.
**Line 120 — Dimmed Color label:** Change:
```svelte
<label>Dimmed Color</label>
<div class="row-controls">
<input
type="color" value={settings.dimmedColor}
```
to:
```svelte
<label for="caption-dimmed-color">Dimmed Color</label>
<div class="row-controls">
<input
id="caption-dimmed-color"
type="color" value={settings.dimmedColor}
```
**Line 165 — BG Opacity label + range:** Wrap input inside label (same as Font Size/Dim Opacity):
```svelte
<label>
BG Opacity
<span class="value">{Math.round(settings.backgroundOpacity * 100)}%</span>
<input
type="range" min="0" max="100" step="5"
value={Math.round(settings.backgroundOpacity * 100)}
oninput={(e) => update({ backgroundOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
/>
</label>
```
Remove the standalone `<input>` below.
**Line 195 — Shadow Depth label + range:** Wrap input inside label:
```svelte
<label>
Shadow Depth
<span class="value">{settings.shadowDepth}px</span>
<input
type="range" min="1" max="5" step="1"
value={settings.shadowDepth}
oninput={(e) => update({ shadowDepth: parseInt((e.target as HTMLInputElement).value, 10) })}
/>
</label>
```
Remove the standalone `<input>` below.
**Line 209 — Position label:** Same as Dimmed Text — heading for radio group:
```svelte
<label for="caption-pos-bottom">Position</label>
<div class="radio-row">
<label>
<input id="caption-pos-bottom" type="radio" name="caption-position" value="bottom"
```
- [ ] **Step 2: Fix ClipList (2 warnings: tabindex + key handler)**
In `ClipList.svelte` line 46, the `<div class="clip-list" role="listbox">` needs `tabindex="0"` and a `onkeydown` handler. Change:
```svelte
<div class="clip-list" role="listbox" onclick={handleContainerClick}>
```
to:
```svelte
<div
class="clip-list"
role="listbox"
tabindex="0"
onclick={handleContainerClick}
onkeydown={(e) => {
if (e.key === 'Escape') {
selectClip(null);
}
}}
>
```
- [ ] **Step 3: Fix PreferencesPanel (2 warnings: tabindex + key handler)**
In `PreferencesPanel.svelte`, change lines 60-61:
```svelte
<div class="overlay" role="presentation" onclick={onClose}>
<div class="panel" role="dialog" onclick={(e) => e.stopPropagation()}>
```
to:
```svelte
<div class="overlay" role="presentation" onclick={onClose} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
<div class="panel" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()}>
```
- [ ] **Step 4: Fix ExportDialog (2 warnings: tabindex + key handler)**
In `ExportDialog.svelte`, change lines 139-140:
```svelte
<div class="overlay" role="presentation" onclick={onClose}>
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>
```
to:
```svelte
<div class="overlay" role="presentation" onclick={onClose} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
<div class="dialog" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()}>
```
- [ ] **Step 5: Fix AboutDialog (2 warnings: tabindex + key handler)**
In `AboutDialog.svelte`, change lines 15-16:
```svelte
<div class="overlay" role="presentation" onclick={onClose}>
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>
```
to:
```svelte
<div class="overlay" role="presentation" onclick={onClose} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
<div class="dialog" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()}>
```
- [ ] **Step 6: Fix VideoPlayer (1 warning: video without track)**
In `VideoPlayer.svelte`, add a `<track>` inside the `<video>` element after the `playsinline` attribute (around line 197):
```svelte
<video
bind:this={videoElement}
src={videoSrc}
ontimeupdate={handleTimeUpdate}
onplay={handlePlay}
onpause={handlePause}
onerror={handleError}
onloadeddata={handleLoadedData}
preload="metadata"
playsinline
>
<track kind="captions" />
</video>
```
Note: change the self-closing `></video>` to wrap the `<track>` element.
- [ ] **Step 7: Fix App.svelte resize handle (1 warning: non-interactive element with mouse handler)**
In `App.svelte`, update the resize handle div (lines 188-193) to add keyboard support:
```svelte
<div
class="resize-handle"
class:active={isResizing}
role="separator"
aria-orientation="horizontal"
aria-valuenow={timelineHeight}
aria-valuemin={40}
aria-valuemax={500}
tabindex="0"
onmousedown={handleResizeStart}
onkeydown={(e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
timelineHeight = Math.min(500, timelineHeight + 10);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
timelineHeight = Math.max(40, timelineHeight - 10);
}
}}
></div>
```
- [ ] **Step 8: Verify all a11y warnings are resolved**
Run:
```bash
npm run check 2>&1
```
Expected: `svelte-check found 0 errors and 0 warnings`
Also verify the production build:
```bash
npm run build 2>&1 | grep -c "svelte"
```
Expected: No a11y warnings in the Vite build output (only the normal build success lines).
- [ ] **Step 9: Commit**
```bash
git add src/lib/components/CaptionSettingsPanel.svelte \
src/lib/components/ClipList.svelte \
src/lib/components/PreferencesPanel.svelte \
src/lib/components/ExportDialog.svelte \
src/lib/components/AboutDialog.svelte \
src/lib/components/VideoPlayer.svelte \
src/App.svelte
git commit -m "fix: resolve all Svelte a11y warnings
- Associate labels with controls in CaptionSettingsPanel (9 warnings)
- Add tabindex and keyboard handlers to dialog overlays (8 warnings)
- Add <track> to video element for caption accessibility
- Add keyboard support to resize handle separator"
```
---
### Task 3: Rust Dead Code Annotations
**Files:**
- Modify: `src-tauri/src/commands/export.rs:23`
- Modify: `src-tauri/src/commands/video.rs:23`
- Modify: `src-tauri/src/services/clip_exporter.rs:10,204,210`
- Modify: `src-tauri/src/services/video_resolver.rs:7`
- Test: `cargo build`
**Interfaces:**
- Consumes: nothing
- Produces: Zero warnings from `cargo build`
- [ ] **Step 1: Annotate `ExportEvent::Error` in `commands/export.rs`**
Add `#[allow(dead_code)]` on the `Error` variant (line 23):
```rust
Finished {
paths: Vec<String>,
},
#[allow(dead_code)] // Reserved for future error-channel reporting
Error {
message: String,
},
```
- [ ] **Step 2: Annotate `DownloadEvent::Error` in `commands/video.rs`**
Add `#[allow(dead_code)]` on the `Error` variant (line 23):
```rust
Finished { success: bool, path: String },
#[allow(dead_code)] // Reserved for future error-channel reporting
Error { message: String },
```
- [ ] **Step 3: Annotate unused functions in `services/clip_exporter.rs`**
Add `#[allow(dead_code)]` on each unused function:
Line 10:
```rust
/// Strip YouTube auto-generated VTT karaoke tags and positioning metadata
/// that confuse ffmpeg's VTT parser / mov_text conversion.
/// Returns the path to a cleaned temp VTT file.
#[allow(dead_code)] // Scaffolded for caption burn-in pipeline
pub fn sanitize_vtt_for_ffmpeg(caption_path: &str, temp_dir: &str) -> Result<String, String> {
```
Line 204:
```rust
#[allow(dead_code)] // Scaffolded for caption burn-in pipeline
fn opacity_to_ass_back_colour(opacity: f64) -> String {
```
Line 210:
```rust
#[allow(dead_code)] // Scaffolded for caption burn-in pipeline
fn caption_style_to_force_style(style: &CaptionStyle) -> String {
```
- [ ] **Step 4: Annotate `SubtitleFormat` in `services/video_resolver.rs`**
Add `#[allow(dead_code)]` on the struct (line 7):
```rust
#[derive(Debug, Deserialize)]
#[allow(dead_code)] // Fields used for serde deserialization
pub struct SubtitleFormat {
pub ext: Option<String>,
pub url: Option<String>,
}
```
- [ ] **Step 5: Verify zero Rust warnings**
Run:
```bash
cd src-tauri && cargo build 2>&1 | grep "warning:"
```
Expected: No output (zero warnings).
- [ ] **Step 6: Commit**
```bash
git add src-tauri/src/commands/export.rs \
src-tauri/src/commands/video.rs \
src-tauri/src/services/clip_exporter.rs \
src-tauri/src/services/video_resolver.rs
git commit -m "fix: suppress Rust dead code warnings with annotations
- ExportEvent::Error and DownloadEvent::Error reserved for future use
- clip_exporter functions scaffolded for caption burn-in pipeline
- SubtitleFormat fields required for serde deserialization"
```
---
### Task 4: Test Fix
**Files:**
- Modify: `tests/lib/stores/clips.test.ts:60-68`
- Test: `npm test`
**Interfaces:**
- Consumes: nothing (test-only change)
- Produces: 31/31 tests passing
- [ ] **Step 1: Update the failing test assertion**
In `tests/lib/stores/clips.test.ts`, change the test "updates selected clip start when I is pressed" (lines 60-68):
```typescript
it('updates selected clip start when I is pressed', () => {
addClip(10, 20);
const id = getClips()[0].id;
selectClip(id);
markInPoint(12);
expect(getClips()[0].startTime).toBe(12);
expect(getPendingInPoint()).toBeNull();
});
```
The change: `markInPoint(5)` → `markInPoint(12)` and `toBe(5)` → `toBe(12)`.
Time 12 is inside the clip (10–20) so `isTimeInsideClip` returns true and the edit path runs. Time 5 was outside the 0.5s tolerance (needed ≥ 9.5).
- [ ] **Step 2: Verify all tests pass**
Run:
```bash
npm test
```
Expected:
```
Test Files 5 passed (5)
Tests 31 passed (31)
```
- [ ] **Step 3: Commit**
```bash
git add tests/lib/stores/clips.test.ts
git commit -m "fix: correct clip in-point edit test for EDIT_TOLERANCE
markInPoint(5) is outside the clip (10-20) with 0.5s tolerance.
Use markInPoint(12) which is inside the clip and triggers the edit path."
```
---
### Task 5: Release Build Script & Packaging
**Files:**
- Create: `scripts/build-release.sh`
- Modify: `src-tauri/tauri.conf.json:38` (bundle targets)
- Modify: `.gitignore` (add `dist/`)
- Modify: `package.json` (add `release` script)
- Modify: `README.md` (add Release Build section)
- Test: `npm run release`
**Interfaces:**
- Consumes: Tasks 1–4 completed (clean builds)
- Produces: `dist/release/GUI Video Clipper.app` — a runnable, ad-hoc signed macOS app bundle
- [ ] **Step 1: Change bundle targets in `tauri.conf.json`**
In `src-tauri/tauri.conf.json`, change line 38 from:
```json
"targets": "all",
```
to:
```json
"targets": ["app", "dmg"],
```
- [ ] **Step 2: Add `dist/` to `.gitignore`**
Append to `.gitignore`:
```
dist/
```
- [ ] **Step 3: Add `release` script to `package.json`**
Add to the `"scripts"` section of `package.json`:
```json
"release": "bash scripts/build-release.sh"
```
Place it after the existing `"tauri"` script.
- [ ] **Step 4: Create `scripts/build-release.sh`**
```bash
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
VERSION_FILE="$PROJECT_ROOT/VERSION"
# ── Read and validate version ──────────────────────────────────────
if [ ! -f "$VERSION_FILE" ]; then
echo "ERROR: VERSION file not found" >&2
exit 1
fi
VERSION=$(cat "$VERSION_FILE" | sed 's/^[vV]//' | tr -d '[:space:]')
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "ERROR: '$VERSION' is not valid semver (expected X.Y.Z)" >&2
exit 1
fi
echo "=== Building GUI Video Clipper v$VERSION ==="
echo ""
# ── Install frontend dependencies ──────────────────────────────────
echo "→ Installing dependencies…"
cd "$PROJECT_ROOT"
npm install --silent
# ── Run tauri build ────────────────────────────────────────────────
echo "→ Building release (this may take a few minutes)…"
npx tauri build 2>&1 | tail -5
# ── Locate built artifacts ─────────────────────────────────────────
# Tauri outputs to src-tauri/target/release/bundle/
BUNDLE_DIR="$PROJECT_ROOT/src-tauri/target/release/bundle"
APP_SRC="$BUNDLE_DIR/macos/GUI Video Clipper.app"
DMG_GLOB="$BUNDLE_DIR/dmg/GUI Video Clipper_*.dmg"
if [ ! -d "$APP_SRC" ]; then
echo "ERROR: .app bundle not found at '$APP_SRC'" >&2
echo "Check the build output above for errors." >&2
exit 1
fi
# ── Ad-hoc code sign ──────────────────────────────────────────────
echo "→ Ad-hoc signing .app…"
codesign --force --deep -s - "$APP_SRC"
# ── Copy to dist/release/ ─────────────────────────────────────────
DIST_DIR="$PROJECT_ROOT/dist/release"
mkdir -p "$DIST_DIR"
echo "→ Copying to dist/release/…"
rm -rf "$DIST_DIR/GUI Video Clipper.app"
cp -R "$APP_SRC" "$DIST_DIR/"
# Copy DMG if it was built
DMG_FILE=$(ls $DMG_GLOB 2>/dev/null | head -1 || true)
if [ -n "$DMG_FILE" ]; then
cp "$DMG_FILE" "$DIST_DIR/"
echo " ✓ DMG: $(basename "$DMG_FILE")"
fi
# ── Summary ────────────────────────────────────────────────────────
APP_SIZE=$(du -sh "$DIST_DIR/GUI Video Clipper.app" | cut -f1)
echo ""
echo "=== Build Complete ==="
echo " Version: v$VERSION"
echo " App: dist/release/GUI Video Clipper.app ($APP_SIZE)"
if [ -n "$DMG_FILE" ]; then
DMG_SIZE=$(du -sh "$DIST_DIR/$(basename "$DMG_FILE")" | cut -f1)
echo " DMG: dist/release/$(basename "$DMG_FILE") ($DMG_SIZE)"
fi
echo ""
echo "To run: open \"dist/release/GUI Video Clipper.app\""
```
Make it executable:
```bash
chmod +x scripts/build-release.sh
```
- [ ] **Step 5: Update README with Release Build section**
Add a new section after "## Build & Run" in `README.md`:
```markdown
## Release Build
```bash
npm run release
```
This runs the full Tauri production build, ad-hoc code signs the `.app` bundle, and copies the output to `dist/release/`.
The resulting `GUI Video Clipper.app` can be launched directly or dragged to `/Applications`.
```
- [ ] **Step 6: Verify the release build**
Run:
```bash
npm run release
```
Expected:
- Script prints version, build progress, and summary
- `dist/release/GUI Video Clipper.app` exists
- App launches: `open "dist/release/GUI Video Clipper.app"`
- [ ] **Step 7: Commit**
```bash
git add scripts/build-release.sh \
src-tauri/tauri.conf.json \
.gitignore \
package.json \
README.md
git commit -m "feat: add release build script
- scripts/build-release.sh: builds, signs, and copies .app to dist/release/
- Bundle targets narrowed to app + dmg
- npm run release convenience script
- README documents the release build workflow"
```
---
## Final Verification
After all tasks, run the full check suite:
```bash
npm run check # 0 errors, 0 warnings
npm test # 31/31 pass
cd src-tauri && cargo build 2>&1 | grep "warning:" # no output
npm run release # produces dist/release/GUI Video Clipper.app
```