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>
This commit is contained in:
2026-09-22 14:09:23 -04:00
parent 1ea7f19668
commit d46973a719
10 changed files with 1001 additions and 17 deletions

View File

@@ -0,0 +1,733 @@
# 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
```