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

21 KiB
Raw Blame History

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:

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:

{
  "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:

import { defineConfig } from 'vite';

to:

import { defineConfig } from 'vitest/config';

This re-export extends UserConfigExport with vitest's test property.

  • Step 4: Verify TS errors are resolved

Run:

npm run check 2>&1 | grep -E "^(Error|.*Error:)"

Expected: No lines matching "Error" — only a11y warnings remain.

  • Step 5: Commit
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:

<label>Font</label>
<select
  value={settings.fontFamily}

to:

<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:

<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:

<label>Text Color</label>
<div class="row-controls">
  <input
    type="color" value={settings.textColor}

to:

<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:

<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):

<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:

<label>Dimmed Color</label>
<div class="row-controls">
  <input
    type="color" value={settings.dimmedColor}

to:

<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):

<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:

<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:

<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:

<div class="clip-list" role="listbox" onclick={handleContainerClick}>

to:

<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:

<div class="overlay" role="presentation" onclick={onClose}>
  <div class="panel" role="dialog" onclick={(e) => e.stopPropagation()}>

to:

<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:

<div class="overlay" role="presentation" onclick={onClose}>
  <div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>

to:

<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:

<div class="overlay" role="presentation" onclick={onClose}>
  <div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>

to:

<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):

    <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:

      <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:

npm run check 2>&1

Expected: svelte-check found 0 errors and 0 warnings

Also verify the production build:

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
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):

    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):

    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:

/// 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:

#[allow(dead_code)] // Scaffolded for caption burn-in pipeline
fn opacity_to_ass_back_colour(opacity: f64) -> String {

Line 210:

#[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):

#[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:

cd src-tauri && cargo build 2>&1 | grep "warning:"

Expected: No output (zero warnings).

  • Step 6: Commit
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):

  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:

npm test

Expected:

 Test Files  5 passed (5)
      Tests  31 passed (31)
  • Step 3: Commit
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:

    "targets": "all",

to:

    "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:

    "release": "bash scripts/build-release.sh"

Place it after the existing "tauri" script.

  • Step 4: Create scripts/build-release.sh
#!/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:

chmod +x scripts/build-release.sh
  • Step 5: Update README with Release Build section

Add a new section after "## Build & Run" in README.md:

## 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

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:

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