chore: add vanilla settings and basic commands for claude

This commit is contained in:
Christian Benincasa
2026-04-12 14:00:00 -04:00
parent 9ab26976c2
commit d40e232960
6 changed files with 339 additions and 0 deletions

20
.claude/commands/check.md Normal file
View File

@@ -0,0 +1,20 @@
---
allowed-tools: Bash(pnpm turbo:*), Bash(pnpm lint-changed:*)
description: Run typechecking, lint, and tests across all packages
---
## Your task
Run the full static analysis and test suite for the monorepo:
1. **Typecheck all packages** — run `pnpm turbo typecheck`
2. **Lint changed files** — run `pnpm lint-changed`
3. **Run all tests** — run `pnpm turbo test`
Run these in sequence. After all three complete, summarize:
- Any type errors (package, file, line)
- Any lint errors or warnings
- Any test failures (suite, test name, error)
- Overall pass/fail status
If everything is clean, say so clearly. Do not attempt to fix errors unless the user asks.

View File

@@ -0,0 +1,21 @@
---
allowed-tools: Bash(cd server && pnpm drizzle-kit:*), Bash(git diff:*), Bash(git status:*)
description: Generate a new Drizzle ORM migration after schema changes
argument-hint: Brief migration description (e.g. "add-channel-tags")
---
## Context
- Current schema changes: !`git diff --name-only HEAD | grep -E "^server/src/db/schema/"`
- Existing migrations: !`ls server/src/migration/`
## Your task
A schema change has been made and needs a migration generated.
1. Confirm there are uncommitted schema changes under `server/src/db/schema/`. If there are none, tell the user and stop.
2. Run `cd server && pnpm drizzle-kit generate` to generate the migration.
3. Show the contents of the newly created migration file.
4. Remind the user to review the migration before committing — Drizzle sometimes generates destructive statements (DROP COLUMN, DROP TABLE) that need manual adjustment.
Migration name hint from arguments: $ARGUMENTS

View File

@@ -0,0 +1,213 @@
---
description: Add a feature to the FFmpeg transcode pipeline following Tunarr's pipeline builder pattern
argument-hint: Brief description of the feature (e.g. "crop filter", "AV1 encoder", "noise reduction filter")
---
# Pipeline Feature Development
You are helping a developer add a feature to Tunarr's FFmpeg transcode pipeline.
Feature request: $ARGUMENTS
---
## Phase 1: Understand the request
Determine which pipeline primitive this feature maps to. Ask the user if unclear:
- **Filter** — a video or audio processing step (scale, deinterlace, denoise, crop, overlay, etc.)
- **Encoder** — a new output codec (video or audio)
- **Decoder** — a new input codec handler
- **Input option** — a per-input CLI flag (e.g. `-readrate`, `-reconnect`)
- **Global option** — a top-level ffmpeg flag (e.g. `-thread_queue_size`)
- **Hardware acceleration variant** — a new hwaccel backend for an existing feature
Also confirm: does this apply to **software only**, or does it need **hardware variants** (VAAPI, NVIDIA/CUDA, QSV, VideoToolbox)?
---
## Phase 2: Explore relevant existing code
Before writing anything, read the existing implementation of the nearest equivalent feature to understand conventions. Use these as canonical references:
| What you're adding | Reference implementation to read first |
|---|---|
| Software video filter | `server/src/ffmpeg/builder/filter/ScaleFilter.ts` |
| Software audio filter | `server/src/ffmpeg/builder/filter/LoudnormFilter.ts` |
| VAAPI hardware filter | `server/src/ffmpeg/builder/filter/vaapi/ScaleVaapiFilter.ts` |
| NVIDIA hardware filter | `server/src/ffmpeg/builder/filter/nvidia/ScaleCudaFilter.ts` |
| Software video encoder | `server/src/ffmpeg/builder/encoder/Libx264Encoder.ts` |
| Hardware encoder (VAAPI) | `server/src/ffmpeg/builder/encoder/vaapi/H264VaapiEncoder.ts` |
| Decoder | `server/src/ffmpeg/builder/decoder/H264Decoder.ts` |
| Input option | `server/src/ffmpeg/builder/options/input/HttpReconnectInputOption.ts` |
| Global option | `server/src/ffmpeg/builder/options/GlobalOption.ts` |
Also read the integration point where the equivalent feature is wired in:
- Software filters: `server/src/ffmpeg/builder/pipeline/software/SoftwarePipelineBuilder.ts`
- Hardware filters: the relevant builder in `server/src/ffmpeg/builder/pipeline/hardware/`
- Encoders/decoders: `server/src/ffmpeg/builder/decoder/DecoderFactory.ts` and the relevant pipeline builder's `setupEncoder()`
---
## Phase 3: Clarify before implementing
Before writing code, present any open questions:
- Does this filter modify the frame format? (affects `FrameState` — resolution, pixel format, color space, codec)
- Should it be conditional (e.g. only when HDR is detected, only when deinterlacing is enabled)?
- What pipeline position does it belong in? (before/after scale, before/after encoder, etc.)
- Is it gated by a user-facing config option, or always applied?
- For hardware features: should it fall back to software if the hwaccel device doesn't support it?
Wait for answers before proceeding.
---
## Phase 4: Implement
### Adding a filter
1. Create the filter class in the correct directory:
- Software: `server/src/ffmpeg/builder/filter/MyFilter.ts`
- VAAPI: `server/src/ffmpeg/builder/filter/vaapi/MyVaapiFilter.ts`
- NVIDIA: `server/src/ffmpeg/builder/filter/nvidia/MyCudaFilter.ts`
- QSV: `server/src/ffmpeg/builder/filter/qsv/MyQsvFilter.ts`
2. Extend the correct base:
- `FilterOption` for software filters
- `HardwareFilterOption` for hardware filters (manage pre/post processing filters)
3. Implement the required contract:
- `get filter(): string` — the ffmpeg filter expression
- `affectsFrameState: boolean` — set `true` if this changes resolution, pixel format, or color format
- `nextState(currentState: FrameState): FrameState` — if `affectsFrameState`, return the updated state using `currentState.update({ ... })`
4. Integrate into the pipeline builder. **Critical: FrameState must be threaded correctly** — each filter receives the state from the previous filter and returns the next:
```typescript
// In SoftwarePipelineBuilder.setupVideoFilters():
if (shouldApplyMyFilter) {
const f = new MyFilter(params);
filterChain.videoFilterSteps.push(f);
if (f.affectsFrameState) {
currentFrameState = f.nextState(currentFrameState);
}
}
```
5. If the filter needs hardware variants, implement those separately and integrate them in the respective hardware pipeline builder (`VaapiPipelineBuilder`, `NvidiaPipelineBuilder`, `QsvPipelineBuilder`). Each hardware builder inherits from `SoftwarePipelineBuilder` and overrides `setupVideoFilters()`.
### Adding an encoder
1. Create the encoder in `server/src/ffmpeg/builder/encoder/` (or the hardware subdirectory).
2. Extend `VideoEncoder` (or `AudioEncoder`).
3. Implement:
- `readonly name: string` — the ffmpeg codec name (e.g. `'libsvtav1'`)
- `readonly videoFormat: VideoFormat` — output format (e.g. `'av1'`)
- `options(): string[]` — codec flags and options
4. Wire into the pipeline builder's `setupEncoder()` method, selecting this encoder when the target format matches.
### Adding a decoder
1. Create in `server/src/ffmpeg/builder/decoder/`.
2. Extend `Decoder` (which extends `InputOption`).
3. Implement `readonly name: string`.
4. Register in `DecoderFactory` for the relevant codec and hardware mode.
### Adding an input or global option
1. Create in `server/src/ffmpeg/builder/options/input/` or `server/src/ffmpeg/builder/options/`.
2. Extend `InputOption` or `ConstantGlobalOption`.
3. Implement `options()` returning the CLI arg array.
4. Add to the pipeline in `BasePipelineBuilder.fromContext()` or the relevant setup method.
---
## Phase 5: Write tests
Every new filter, encoder, or pipeline integration needs two layers of tests.
### Unit tests
Place alongside the implementation file (e.g. `MyFilter.test.ts` next to `MyFilter.ts`). These run without real FFmpeg or hardware.
**What to test for a filter:**
- The `filter` string is correct for each relevant input scenario (different resolutions, pixel formats, frame locations)
- If `affectsFrameState` is `true`, assert that `nextState()` returns the expected `FrameState` fields
- If the filter handles hardware frame location (CPU vs GPU), test both paths — the filter should emit a `hwdownload` prefix when frames are on the hardware device
Use `PadFilter.test.ts` as the reference — it covers software, VAAPI 8-bit, VAAPI 10-bit, and CUDA variants with explicit `FrameState` construction.
**What to test for a pipeline builder integration:**
- Build a pipeline with mocked/stub capabilities and assert that the generated command args (`pipeline.getCommandArgs()`) contain the expected filter or option in the expected position
- Test the conditional branches: feature enabled, feature disabled, HDR vs SDR, hardware available vs unavailable
Use `VaapiPipelineBuilder.test.ts` as the reference — it constructs `FfmpegState` and `FrameState` directly and asserts on specific filter types appearing in the pipeline.
**Test setup conventions:**
- Use `FrameState` and `FfmpegState` constructors directly — no DI required
- Create fake `FfmpegCapabilities` via `EmptyFfmpegCapabilities` or stub the relevant `canHandle*` methods
- Use `VideoStream.create(...)` with explicit fields rather than probing real files
### Live integration tests
Place in the same directory as the pipeline builder test, with a `.local.test.ts` suffix (e.g. `SoftwarePipeline.local.test.ts`). These run actual FFmpeg against real fixture files and verify the output is valid media.
**Setup:**
- Import `binaries`, `Fixtures`, `vaapiTest`/`nvidiaTest`/`qsvTest` from `server/src/testing/ffmpeg/FfmpegTestFixtures.ts`
- Use `createTempWorkdir()` from `server/src/testing/ffmpeg/FfmpegIntegrationHelper.ts` for output paths — always clean up in `afterEach`
- Cap test duration to 1 second using `dayjs.duration(1, 'second')` to keep the suite fast
- Use `deriveVideoStreamForFixture(Fixtures.video1080p)` to get a real `VideoInputSource` with correct stream metadata
**Hardware test skipping:**
- Hardware tests must use the extended test fixtures (`vaapiTest`, `nvidiaTest`, `qsvTest`) which auto-skip if the hardware device is not available
- Never use a plain `test()` for hardware paths — it will fail in CI
**What to assert:**
- `runFfmpegWithPipeline()` exits with code `0`
- `probeFile()` on the output file confirms the expected codec, resolution, or pixel format was applied
- If the feature is conditional (e.g. only fires on HDR input), test with `Fixtures.videoHevc1080p` (HDR) and a non-HDR fixture and assert the difference
**Available fixtures** (`server/src/testing/ffmpeg/fixtures/`):
- `Fixtures.video720p` — 720p H.264
- `Fixtures.video1080p` — 1080p H.264
- `Fixtures.video480p43` — 480p H.264 4:3
- `Fixtures.videoHevc720p` / `Fixtures.videoHevc1080p` — HDR10 HEVC
- `Fixtures.watermark` / `Fixtures.blackWatermark` — watermark PNGs
**Environment variables for local hardware testing:**
- `TUNARR_TEST_FFMPEG` / `TUNARR_TEST_FFPROBE` — override binary paths
- `TUNARR_TEST_VAAPI_DEVICE` — override VAAPI device (default `/dev/dri/renderD128`)
### Running the tests
```bash
# Unit tests only (fast, no hardware required)
cd server && pnpm test src/ffmpeg/
# Single test file
cd server && pnpm test src/ffmpeg/builder/filter/MyFilter.test.ts
# Integration tests (requires real FFmpeg and optionally hardware)
cd server && pnpm test src/ffmpeg/builder/pipeline/software/SoftwarePipeline.local.test.ts
```
---
## Phase 6: Verify
After implementing and writing tests:
1. Run `cd server && pnpm typecheck` — the pipeline step type system is strict and will catch interface mismatches.
2. Run `cd server && pnpm test src/ffmpeg/` to confirm all unit tests pass.
3. If adding a hardware variant, confirm the software path is still exercised correctly when hardware is unavailable — hardware builders fall back through `SoftwarePipelineBuilder`.
4. Confirm `FrameState` threading is correct: trace through `setupVideoFilters()` and verify state is updated after every filter that sets `affectsFrameState = true`.
---
## Key architecture reminders
- **Never mutate `FrameState` directly** — always use `currentState.update({ ... })` which returns a new instance.
- **`FilterChain` has four slots**: `videoFilterSteps`, `subtitleOverlayFilterSteps`, `watermarkOverlayFilterSteps`, `pixelFormatFilterSteps` — put filters in the right one.
- **Hardware filters manage frame location** — frames must be uploaded to device memory before hardware filters and downloaded after if software encoding follows. Use the existing upload/download filter patterns.
- **`HardwareFilterOption` pre/post filters** — use `preprocessFilters` and `postProcessFilters` arrays, not ad-hoc insertions.
- **`PipelineBuilderFactory`** — if adding a new hwaccel mode end-to-end, register the new builder here.

34
.claude/commands/pr.md Normal file
View File

@@ -0,0 +1,34 @@
---
allowed-tools: Bash(git log:*), Bash(git diff:*), Bash(git status:*), Bash(git branch:*), Bash(git push:*), Bash(gh pr create:*), Bash(gh pr view:*)
description: Create a pull request targeting the correct branch per project conventions
argument-hint: Optional PR title or description hint
---
## Context
- Current branch: !`git branch --show-current`
- Commits ahead of main: !`git log --oneline origin/main..HEAD`
- Commits ahead of dev: !`git log --oneline origin/dev..HEAD`
- Changed files: !`git diff --name-only origin/main..HEAD`
- Git status: !`git status --short`
## Your task
Create a pull request following Tunarr's branching conventions:
**Branch targeting rules:**
- `fix` commits → target `main`
- `feat` commits and large changes → target `dev`
- If the commits are mixed or ambiguous, ask the user which branch to target before proceeding
**Steps:**
1. Determine the correct target branch from the commit types above
2. If there are uncommitted changes, stop and tell the user to commit first
3. Push the current branch to origin if not already pushed
4. Draft a PR title and body based on the commits and diff:
- Title: concise, follows conventional commit style
- Body: summary of what changed and why, plus a test plan checklist
5. Create the PR with `gh pr create` targeting the correct branch
6. Return the PR URL
Hint from arguments: $ARGUMENTS

View File

@@ -0,0 +1,19 @@
---
allowed-tools: Bash(cd server && pnpm generate-openapi:*), Bash(cd web && pnpm generate-client:*), Bash(cd web && pnpm regen-routes:*), Bash(git diff:*)
description: Regenerate the OpenAPI spec and web API client after server API changes
---
## Context
- Changed API/types files: !`git diff --name-only HEAD | grep -E "^(server/src/api/|types/src/)"`
## Your task
Regenerate the full API contract chain after server-side changes:
1. **Generate OpenAPI spec** — run `cd server && pnpm generate-openapi`
2. **Regenerate web client** — run `cd web && pnpm generate-client`
3. **Regenerate TanStack Router routes** — run `cd web && pnpm regen-routes`
4. Show a summary of what changed: `git diff --stat -- web/src/generated/`
Do these steps in order (each depends on the previous). Report any errors immediately and stop.

32
.claude/settings.json Normal file
View File

@@ -0,0 +1,32 @@
{
"permissions": {
"allow": [
"Bash(pnpm turbo:*)",
"Bash(pnpm fmt:*)",
"Bash(pnpm lint-changed:*)",
"Bash(pnpm build:*)",
"Bash(pnpm test:*)",
"Bash(pnpm i:*)",
"Bash(pnpm typecheck:*)",
"Bash(pnpm drizzle-kit:*)",
"Bash(cd server && pnpm test:*)",
"Bash(cd server && pnpm generate-openapi:*)",
"Bash(cd server && pnpm drizzle-kit:*)",
"Bash(cd server && pnpm typecheck:*)",
"Bash(cd web && pnpm generate-client:*)",
"Bash(cd web && pnpm regen-routes:*)",
"Bash(cd web && pnpm typecheck:*)",
"Bash(git log:*)",
"Bash(git diff:*)",
"Bash(git status:*)",
"Bash(git show:*)",
"Bash(git blame:*)",
"Bash(git branch:*)",
"Bash(gh issue view:*)",
"Bash(gh issue list:*)",
"Bash(gh pr view:*)",
"Bash(gh pr list:*)",
"Bash(gh pr checks:*)"
]
}
}