diff --git a/.changeset/album-artist-browsing-capability.md b/.changeset/album-artist-browsing-capability.md deleted file mode 100644 index 5bc1b087..00000000 --- a/.changeset/album-artist-browsing-capability.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Add capability-gated clean artists transform - -Devices now declare whether they use Album Artist for browse navigation via `supportsAlbumArtistBrowsing`. When enabled globally, the `cleanArtists` transform is automatically suppressed on devices that support Album Artist browsing (Rockbox, Echo Mini, generic) and auto-applied on devices that don't (iPod). Per-device overrides still take priority. - -The dry-run summary shows when the transform is skipped (`Clean artists: skipped (device supports Album Artist browsing)`), and warns when it's force-enabled on a capable device. Both `sync --dry-run` and `device info` surface these in text and JSON output. diff --git a/.changeset/album-artist-path-template.md b/.changeset/album-artist-path-template.md deleted file mode 100644 index d2bb8b83..00000000 --- a/.changeset/album-artist-path-template.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Fix mass-storage directory structure to use album artist instead of track artist, and add template-based path system with self-healing relocate. - -**Bug fix:** Mass-storage devices (Echo Mini, Rockbox) now use `albumArtist` for directory grouping, falling back to `artist` when absent. Previously, compilation/various-artist albums had their tracks scattered across separate artist directories instead of being grouped together under the album artist. - -**Path templates:** File paths are now generated from a configurable template string (`{albumArtist}/{album}/{trackNumber} - {title}{ext}` by default). This lays the groundwork for user-customisable folder structures in a future release. - -**Self-healing relocate:** When source metadata changes (e.g. album artist corrected) or the path template changes, the next sync detects the path mismatch and moves files to their correct location via `fs.rename()` — no re-copying of audio data. Relocate operations appear in dry-run output and are tracked as a new `relocate` operation type. diff --git a/.changeset/artwork-album-cache-codec-determinism.md b/.changeset/artwork-album-cache-codec-determinism.md deleted file mode 100644 index 74ef96e3..00000000 --- a/.changeset/artwork-album-cache-codec-determinism.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@podkit/core': patch ---- - -Fix two artwork/codec sync bugs surfaced by the art-matrix test suite: - -- **Album artwork is now deterministic regardless of track scan order.** The album-level artwork cache previously remembered the first track's extraction result for the whole album — so whether a WAV/OGG/Opus track ended up with the album cover depended on which sibling was processed first (and differed between the directory and Subsonic adapters). The cache now pre-resolves each album from a preference-ordered candidate list (embed-capable containers first) and no longer caches a negative result in single-source mode, so every track in an album inherits the same cover. -- **MP3 no longer fires a spurious `codec-changed` re-copy on every sync.** `postProcessCodecChanges` assumed any lossy source would be transcoded to the resolved lossy codec; for an MP3 source on an MP3-capable device the classifier actually direct-copies, so the codec comparison fired `upgrade-direct-copy: codec-changed` on each incremental sync. The pass now asks the classifier and skips when the source is copied rather than transcoded. diff --git a/.changeset/audio-normalization-capability.md b/.changeset/audio-normalization-capability.md deleted file mode 100644 index 6f488249..00000000 --- a/.changeset/audio-normalization-capability.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Add audioNormalization device capability for device-appropriate Sound Check / ReplayGain handling - -Devices now declare their normalization support: 'soundcheck' (iPod), 'replaygain' (Rockbox), or 'none' (Echo Mini, generic). Devices with no normalization support skip soundcheck upgrade detection entirely, and the dry-run output hides or relabels the normalization line accordingly. Configurable via `audioNormalization` in device config. diff --git a/.changeset/audio-normalization-refactoring.md b/.changeset/audio-normalization-refactoring.md deleted file mode 100644 index f14870b5..00000000 --- a/.changeset/audio-normalization-refactoring.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@podkit/core": minor -"podkit": minor ---- - -Refactor audio normalization from iPod-centric Sound Check to a generic `AudioNormalization` type, and add ReplayGain album gain/peak support - -**Normalization refactoring:** - -- Introduce `AudioNormalization` type that preserves source format fidelity (ReplayGain dB, iTunNORM soundcheck integers) without unnecessary round-trip conversions -- Replace scattered `soundcheck`, `soundcheckSource`, `replayGainTrackGain`, `replayGainTrackPeak` fields on `CollectionTrack` with a single `normalization` field -- Replace `soundcheck`, `replayGainTrackGain`, `replayGainTrackPeak` fields on `DeviceTrackInput` with `normalization` -- Conversions now happen at device boundaries: iPod adapter reads soundcheck integers, mass-storage adapter reads dB values directly -- Upgrade detection compares in dB space with 0.1 dB epsilon tolerance, eliminating false positives from integer rounding -- Metadata update diffs show human-readable dB values (e.g., `normalization: -7.5 dB → -6.2 dB`) instead of opaque integers - -**Album gain/peak support (TASK-253):** - -- Extract `albumGain` and `albumPeak` from local file metadata and Subsonic API -- Write `REPLAYGAIN_ALBUM_GAIN` and `REPLAYGAIN_ALBUM_PEAK` via FFmpeg metadata flags during transcode -- Write album gain/peak via node-taglib-sharp tag writer for M4A files -- Thread album data through the full sync pipeline for mass-storage devices (Rockbox, etc.) - -**Breaking changes:** - -- `CollectionTrack` shape: four normalization fields replaced by single `normalization?: AudioNormalization` -- `SoundCheckSource` type removed, replaced by `NormalizationSource` -- Upgrade reason `'soundcheck-update'` renamed to `'normalization-update'` in JSON output -- `soundCheckTracks` stat renamed to `normalizedTracks` diff --git a/.changeset/bundling-pipeline-externals.md b/.changeset/bundling-pipeline-externals.md deleted file mode 100644 index c9f197f5..00000000 --- a/.changeset/bundling-pipeline-externals.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"podkit": patch -"@podkit/ipod-firmware": patch ---- - -Externalize `koffi` and `usb` from the published `bun build` bundles. Koffi loads its native binding via `eval('require')(filename)`; bun's bundler shims top-level `require` as `__require` (via `createRequire(import.meta.url)`) but does not inject `require` into eval'd literals, so the bundled CLI hit `ReferenceError: require is not defined` whenever the SCSI inquiry path was actually reached. The native loaders are now resolved at runtime via `node_modules`, which is also more correct for `usb` (whose `bun build`-time prebuild only matched the build host's platform). - -The standalone-binary path (`bun --compile` via `compile.sh`) is unchanged — it stages platform-specific `.node` files and uses static `require()` in `compile-entry.js`, which works correctly. A bug in `compile.sh`'s linux-arm64 branch is also fixed: the script previously constructed `linux-arm64/node.napi.${USB_VARIANT}.node` (where `USB_VARIANT` is `glibc` or `musl`) but the `usb` package only ships `linux-arm64/node.napi.armv8.node` — no glibc/musl split exists for arm64. The script now selects the armv8 prebuild unconditionally on arm64. - -`@podkit/ipod-firmware` is also externalized from the `@podkit/core` and `@podkit/devices-ipod` builds, so neither package's `dist/index.js` re-inlines firmware (and therefore koffi/usb imports). Bundle content-check tests under `packages/*/src/bundle.test.ts` assert that no `eval("require")` slips into any published bundle. diff --git a/.changeset/clean-artists-device-set.md b/.changeset/clean-artists-device-set.md deleted file mode 100644 index 3cb0c6e7..00000000 --- a/.changeset/clean-artists-device-set.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"podkit": patch ---- - -Add `--clean-artists` / `--no-clean-artists` / `--clear-clean-artists` options to `podkit device set` - -The clean artists transform can now be toggled per-device from the CLI instead of requiring manual config file edits. diff --git a/.changeset/cli-error-shape-collection.md b/.changeset/cli-error-shape-collection.md deleted file mode 100644 index c7c9819d..00000000 --- a/.changeset/cli-error-shape-collection.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"podkit": minor ---- - -Unify JSON error shape for `device add` and `collection music`/`collection video` - -These commands now emit the same JSON error format on failure: - -```json -{ - "success": false, - "error": "", - "code": "", - "...details": "" -} -``` - -**Breaking for `collection music` / `collection video` JSON consumers.** The previous shape was `{ "error": true, "message": "..." }`. If you parse JSON output from these commands, update consumers to read `success === false` and `error` (instead of `error === true` and `message`). - -`device add` errors now also include a `code` field (additive, not breaking). - -Underneath: the runners (`runDeviceAdd`, `runCollectionMusic`, `runCollectionVideo`) throw a typed `CliError` and the action wrapper (`runAction`) translates it into structured output + exit code. Tests assert on the captured JSON instead of `process.exitCode` side-effects. - -Per CLI breaking-change convention this is a minor bump. Other commands still emit their existing shapes; that unification will land in a follow-up. diff --git a/.changeset/cli-error-shape-unified.md b/.changeset/cli-error-shape-unified.md deleted file mode 100644 index 9bb82538..00000000 --- a/.changeset/cli-error-shape-unified.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -"podkit": minor ---- - -Unify and harden CLI JSON error output across every command (ADR-015) - -## What changed - -Every CLI command now emits the same canonical JSON shape on failure. The shape, exit codes, and consumer ergonomics all changed in one breaking pass. - -### Canonical error shape - -```json -{ - "success": false, - "error": "", - "code": "", - "details": { "": "..." } -} -``` - -`code` is required and machine-readable (e.g. `MOUNT_REQUIRES_SUDO`, `FFMPEG_UNAVAILABLE`). `details` is **nested**, not spread at the top level — so command-specific extras can't accidentally collide with `success`/`error`/`code`. - -### Exit codes - -- `0` — success -- `1` — command error (any `CliError` thrown) -- `2` — ran cleanly but found problems (`doctor` reporting unhealthy device, `sync` reporting partial track failures). Carries a `status` field on the success-shape JSON: `'ok' | 'issues-found' | 'partial-failure'`. -- `130` — SIGINT (interrupted sync) - -### Per-command typed error codes - -Every command exports an exhaustive enum of its possible error codes: - -```ts -import { MountErrorCodes, type MountErrorCode } from 'podkit/commands/mount'; -// MountErrorCodes.DEVICE_NOT_RESOLVED, MountErrorCodes.MOUNT_REQUIRES_SUDO, etc. -``` - -A repo-wide barrel is at `packages/podkit-cli/src/commands/error-codes.ts` exporting `PodkitErrorCode` — the union of every code any podkit command may emit. - -### Discriminated `*Output` types - -Each command's output type is now a discriminated union: - -```ts -export type MountOutput = MountSuccess | MountErrorOutput; -``` - -Consumers narrow with `if (output.success) { ... }`. - -## Breaking for JSON consumers - -| Old shape | New shape | -|-----------|-----------| -| `{ error: true, message: "..." }` (collection music/video) | `{ success: false, error, code, details }` | -| `{ success: false, error: "..." }` (no code, top-level extras) | `{ success: false, error, code, details: {...} }` | -| Per-command extras at top level (e.g. `dryRun`, `device`) | Now nested under `details` | -| `process.exitCode === 1` for "found issues" | Now `2`; `1` is reserved for command errors | - -Update parsers to: - -1. Branch on `success === false`. -2. Read `code` for machine-readable tags. -3. Read `details.X` instead of `output.X` for command-specific extras. -4. Branch on exit code 2 for "ran cleanly with issues" (sync partial failure, doctor unhealthy). - -## New ergonomics - -`packages/podkit-cli/src/test-utils/cli-error.ts` and `test-packages/e2e-tests/src/helpers/cli-error.ts` export `expectCliError` for asserting on the canonical shape in one call. - -`OutputContext` now takes an optional `ExitCodeSink` (default: writes `process.exitCode`; tests use `BufferExitCodeSink` to avoid process-global mutation). - -Per CLI breaking-change convention this is a minor bump. diff --git a/.changeset/codec-vorbis-rename-and-container-types.md b/.changeset/codec-vorbis-rename-and-container-types.md deleted file mode 100644 index 26734fc7..00000000 --- a/.changeset/codec-vorbis-rename-and-container-types.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor -"@podkit/device-types": minor -"@podkit/devices-mass-storage": minor -"@podkit/devices-ipod": minor ---- - -Disambiguate codec from container: `AudioCodec` value `'ogg'` is renamed to `'vorbis'` - -The `AudioCodec` slot previously used `'ogg'` to mean "OGG Vorbis." That conflated the OGG container with the Vorbis stream codec and could not represent Vorbis-in-OGG vs Opus-in-OGG as distinct device capabilities — Echo Mini (which plays Vorbis but hides `.opus` files) could not be modelled accurately. The codec slot now names the audio stream codec; `'vorbis'` replaces `'ogg'` in device presets and config. - -Configs containing `supportedAudioCodecs = ["…", "ogg", "…"]` under `[devices.*]` are migrated automatically by `podkit migrate` (config version 1 → 2). The migration is purely a string substitution inside the `supportedAudioCodecs` array; comments and surrounding formatting are preserved. - -Also lands as type-level groundwork for the future container-aware sync work: `AudioContainer`, `AUDIO_CONTAINERS`, `CODEC_CANONICAL_CONTAINER`, and an optional `DeviceCapabilities.containerConstraints` field. These are declared and exported but not yet read by the planner; they are placeholders for the upcoming Phase 2 work documented in the container-aware sync PRD. - -`DirectoryAdapter` now uses each `.ogg` file's probed stream codec (already populated by `music-metadata`) to distinguish Vorbis, Opus, and OGG-FLAC — same pattern as the existing AAC/ALAC distinction for `.m4a`. `SubsonicAdapter` additionally checks the API's `contentType` field for Opus-in-`.ogg`. The Subsonic check is best-effort because most Subsonic servers report container MIME (`audio/ogg`) regardless of stream codec; deeper probing is deferred until evidence of real-world impact. - -User-facing reference page added at `docs/reference/codec-support.md` explaining the codec/container model and what each `AudioCodec` value means. diff --git a/.changeset/completions-namespace-isolation.md b/.changeset/completions-namespace-isolation.md deleted file mode 100644 index 48735238..00000000 --- a/.changeset/completions-namespace-isolation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"podkit": patch ---- - -Fix shell completions namespace conflict when multiple podkit binaries are installed. - -The `--cmd` flag now derives the completion function prefix from the binary name (`podkit-dev` → `_podkit_dev`), so `podkit` and `podkit-dev` each get an isolated namespace and their completion scripts no longer clobber each other. The `podkit-dev` binary built via `install:dev` now reports a `-dev` version suffix. diff --git a/.changeset/configurable-codec-preferences.md b/.changeset/configurable-codec-preferences.md deleted file mode 100644 index 7431b401..00000000 --- a/.changeset/configurable-codec-preferences.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Add configurable codec preference system for multi-device audio format support - -Users can now configure an ordered list of preferred audio codecs globally and per-device. The system walks the list top-to-bottom, selecting the first codec that is both supported by the target device and has an available FFmpeg encoder. This replaces the hardcoded AAC-only transcoding pipeline. - -- **Default lossy stack:** opus → aac → mp3 (Rockbox devices get Opus automatically, iPods fall through to AAC) -- **Default lossless stack:** source → flac → alac (lossless files are kept in their original format when possible) -- **Quality presets are codec-aware:** "high" delivers perceptually equivalent quality regardless of codec (e.g., Opus 160 kbps ≈ AAC 256 kbps) -- **Codec change detection:** changing your codec preference re-transcodes affected tracks on the next sync -- **`podkit device info`** shows your codec preference list with supported/unsupported codecs marked -- **`podkit sync --dry-run`** shows which codec will be used and any codec changes -- **`podkit doctor`** warns when FFmpeg is missing an encoder for a preferred codec - -Configure via `config.toml`: - -```toml -[codec] -lossy = ["opus", "aac", "mp3"] -lossless = ["source", "flac", "alac"] - -[devices.myipod.codec] -lossy = "aac" -``` - -No configuration is required — existing setups work unchanged with sensible defaults. diff --git a/.changeset/consolidate-unsupported-reason.md b/.changeset/consolidate-unsupported-reason.md deleted file mode 100644 index 34e19991..00000000 --- a/.changeset/consolidate-unsupported-reason.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor -"@podkit/devices-ipod": minor -"@podkit/device-types": minor ---- - -Consolidate the two ways podkit expressed "this device is unsupported" into one canonical shape. `ReadinessUnsupportedReason` moves to `@podkit/device-types` (its natural home), and `resolveIpodModel(bag)` now returns it directly on `IpodModel.unsupportedReason` instead of the bare-string `notSupportedReason`. The bridge functions in `@podkit/core` (`makeUnsupportedReasonFromModel`, `makeUnsupportedReasonFromAssessment`) are removed — consumers read `model.unsupportedReason` directly. Internal refactor; user-facing CLI behaviour is unchanged. diff --git a/.changeset/convergent-track-metadata.md b/.changeset/convergent-track-metadata.md deleted file mode 100644 index ba62334d..00000000 --- a/.changeset/convergent-track-metadata.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Fix track metadata convergence on mass-storage devices and add transfer-mode-aware on-disk tag writes for iPod portable. - -**Bug fix (mass-storage)**: `MassStorageAdapter.updateTrack` previously only wrote `comment`, OGG/Opus artwork, and ReplayGain to disk. All other metadata fields (title, artist, album, albumArtist, genre, year, trackNumber, discNumber, compilation) updated in-memory only — the file's embedded tags on the device were never rewritten. After a relocate or metadata-correction sync the next sync re-detected the same diff every time, looping forever as a zero-byte `update-metadata` op. - -`MassStorageAdapter` now queues every changed textual tag in a single `pendingTagWrites` map and flushes them as one `writeTags(filePath, fields)` call per file via `Promise.allSettled`. Per-file failures are aggregated and re-thrown so the sync executor can categorise them. - -**New behaviour (iPod portable)**: `IpodDeviceAdapter` now mirrors iTunesDB metadata into the on-disk file tags when `transferMode === 'portable'`. This makes files pulled off the iPod self-describing for re-import into a music library. `fast` and `optimized` modes still touch iTunesDB only — the iPod firmware reads metadata from iTunesDB and never falls back to file tags during playback, so paying the tag-rewrite cost in those modes would be wasted work. - -Tag writes are best-effort on iPod portable: failures are surfaced as warnings, not hard errors, because the iTunesDB write (the authoritative store for playback) already succeeded. - -**`addTrack` consistency**: When `transferMode === 'portable'`, both backends now also rewrite tags on first transfer to honour any collection-adapter transforms (e.g. clean-artists, Subsonic-side corrections) that FFmpeg's `-map_metadata 0` would otherwise copy through from the source. - -**On first sync after upgrade**: Existing mass-storage tracks will likely report a `metadata-correction` op on the next sync as stale on-disk tags converge to source values. These are zero-byte writes — no transcoding or transfers happen — but the operation list will look longer than usual for one cycle. - -**Scope notes**: -- Match-key changes (title, artist, album corrections) still produce a remove+add rather than a metadata update. By design: when those fields change, podkit treats it as a different track. -- Virtual-iPod (m-17) inherits the iPod behaviour automatically; no changes needed there. diff --git a/.changeset/core-capability-adapter.md b/.changeset/core-capability-adapter.md deleted file mode 100644 index 55773a72..00000000 --- a/.changeset/core-capability-adapter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@podkit/core": minor ---- - -Add capability adapter and fix iPod generation metadata - -- Add `createIpodCapabilities()` adapter — maps libgpod device data to core `DeviceCapabilities`, using libgpod as authority for video/artwork support and supplementing codec support from generation metadata -- Add `toLibgpodGeneration()` mapping from detection-layer IDs (`nano_4g`) to libgpod IDs (`nano_4`) -- Fix artwork resolution: nano 3G→320, nano 4-6G→240, photo→320 (were all incorrectly 176) -- Fix ALAC support: add to 4th gen, Photo, Mini 2G, Touch 1-4, iPhone 1-4, iPad 1 -- Add video profiles for Touch, iPhone, and iPad generations (were missing, preventing video sync) diff --git a/.changeset/cross-process-sync-coordination.md b/.changeset/cross-process-sync-coordination.md deleted file mode 100644 index eab0e3e6..00000000 --- a/.changeset/cross-process-sync-coordination.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'podkit': minor -'@podkit/core': minor -'@podkit/daemon': minor ---- - -Cross-process sync coordination: per-device lock, transcode owner-liveness, phantom auto-prune. - -**Per-device sync lock.** `podkit sync` now acquires a per-device PID-file lock at `.podkit/sync.lock` (mass-storage) or `iPod_Control/.podkit-sync.lock` (iPod) immediately after opening the device. A second concurrent `podkit sync` against the same device exits with the new `LOCK_HELD` code (exit code **4**) and a message naming the holding PID: - -``` -Error: Another podkit process is already syncing /Volumes/TERAPOD (pid 12345). Wait for it to finish or kill it. -``` - -Crash-safe: the lock file is unlinked in `finally`; if a process is SIGKILLed mid-sync, the next attempt detects the dead PID via liveness probe and takes over cleanly. `podkit sync --dry-run` does **not** take the lock (read-only by design). The daemon (`@podkit/daemon`) detects `LOCK_HELD` from the CLI subprocess and skips that cycle (no retry-spin). Read commands (`device scan`, `device info`, `device music`) are unaffected — writes only. - -**Transcode-tmp owner-liveness.** Each `podkit-transcode-/` scratch directory now writes a small `.owner` file at creation. The pre-sync sweep walker reaps dirs whose owner is dead or whose `.owner` is missing; live owners are left alone. This replaces the previous session-start-time floor, which missed the most common interruption case for the long-running daemon: its own prior cycle. - -**Phantom manifest auto-prune.** When the pre-sync sweep detects manifest rows whose backing audio file has vanished (mass-storage devices), it now prunes them atomically as part of the sweep. Previously the sweep emitted an advisory warning recommending `podkit doctor --repair orphan-files`; the advisory now fires only if the auto-prune itself fails. `doctor --repair orphan-files` remains as a backstop and is unchanged. - -**Filesystem support.** The shared liveness primitive uses only `O_CREAT|O_EXCL`, `unlink`, `rename`, `readFile`, `writeFile`, and `process.kill(pid, 0)` — stable across exFAT, FAT32, HFS+, APFS, ext4, and NTFS. We deliberately avoided `flock(2)` because its semantics on FAT-family filesystems (common for iPods) are platform-dependent. - -**Internal API additions** (`@podkit/core`): `acquireLock`, `LockHandle`, `LockHeldError`, `LockContestedError`, `getOwnIdentity`, `readOwnership`, `writeOwnership`, `isAlive`, `PidFileEntry` — all from a new `lib/pid-file.ts` primitive. `DeviceAdapter` gains an optional `prunePhantomManifest?(paths)` method (implemented on mass-storage, intentionally omitted on iPod). - -**Removed internals:** `SESSION_START_MS` constant and `sessionStartMsOverride` plumbing in `pre-sync-sweep.ts` are gone — replaced wholesale by the PID-file liveness probe. - -See `documents/architecture/sync/planning.md` §6 for the cross-process coordination design. diff --git a/.changeset/device-add-empty-identity-refuse.md b/.changeset/device-add-empty-identity-refuse.md deleted file mode 100644 index 7294c4ab..00000000 --- a/.changeset/device-add-empty-identity-refuse.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"podkit": minor ---- - -**Behavioural change:** `podkit device add` now refuses to persist a device when the identity cascade resolves nothing at all — no SysInfoExtended path, no classic SysInfo on disk, and no USB fingerprint. Previously such devices were silently persisted with empty identity, which stranded subsequent commands (`podkit doctor -d `, `podkit sync -d `) that rely on identity to track the device across replug cycles. - -The refusal exits with code `1`, prints an actionable error (`EMPTY_IDENTITY`), and points to three remediation paths: - -- Re-mount the device read-write and check the USB connection, then retry -- Pass `--no-firmware-inquiry` if you knowingly want to skip the firmware inquiry step -- Pass the new `--force` flag to add the device anyway with a warning - -Partial-cascade scenarios (SysInfoExtended present but USB fingerprint unresolved, or similar) continue to proceed silently — the warning is reserved for cases where neither SysInfoExtended nor classic SysInfo can be read, which is the genuinely actionable signal. diff --git a/.changeset/device-add-slick-flow.md b/.changeset/device-add-slick-flow.md deleted file mode 100644 index 580ea138..00000000 --- a/.changeset/device-add-slick-flow.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"podkit": patch -"@podkit/core": patch ---- - -Redesign `podkit device add` to be slick and informative. Previously, plugging in a post-2006 iPod (nano 2G, nano 7G, iPod 5G) and running `device add` displayed the device as `Model: Invalid` (libgpod's wording for an empty SysInfo file) and instructed the user to manually write a SysInfo file with `ModelNumStr: MA147` — neither friendly nor accurate. - -The new flow: - -1. **Identity is cascade-resolved** from USB product ID, classic SysInfo, SysInfoExtended, and serial — whichever sources are available. Display reads `Found iPod nano (2nd Generation):` rather than `Model: Invalid`. -2. **A single combined prompt** asks `Add this iPod as "X" and write SysInfoExtended? [Y/n]` when SysInfoExtended is missing and USB is reachable. Confirming triggers firmware inquiry, writes SysInfoExtended, and persists to config in one step. -3. **Capabilities are derived from the cascade-resolved generation**, not from libgpod's pessimistic fallback. Negative capabilities cite the reason (`- Video (not supported on iPod nano 4GB Green (2nd Generation))`). -4. **The follow-up tip** suggests `podkit sync -d --dry-run`, not "go run two more commands". - -New flag `--no-firmware-inquiry` skips the firmware fetch+write when used with `--yes` — for the case where the user wants to defer the write or doesn't have the device connected over USB. - -Internal API changes in `@podkit/core`: - -- **Added** `assessIpodIdentity(mountPoint, opts?)` returning `IpodIdentityAssessment` — pure cascade-driven assessment (no writes). Combines all available identification sources and returns `{ model, capabilities, firmwareInquiry: 'present' | 'missing' | 'unwritable', needsChecksum }`. The CLI now composes from this primitive instead of reaching into libgpod for identity. - -The misleading `device-validation.ts` warning text (`Ensure /Volumes/X/iPod_Control/Device/SysInfo exists with your model number (e.g., "ModelNumStr: MA147")`) has been replaced with a pointer to the canonical fix: `podkit doctor --repair sysinfo-extended`. diff --git a/.changeset/device-add-volume-uuid-required.md b/.changeset/device-add-volume-uuid-required.md deleted file mode 100644 index da6efc93..00000000 --- a/.changeset/device-add-volume-uuid-required.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"podkit": patch ---- - -`podkit device add` now refuses cleanly when an iPod's volume UUID can't be read, with a clear message + structured error code (`VOLUME_UUID_REQUIRED`). Previously a synthetic `manual-...` UUID could be persisted in config, which then broke replug detection and `podkit doctor -d ` lookups. Most-common cause (HFS+ on Linux) was already addressed in TASK-317.12; this is the defensive catch-all for any remaining edge cases (corrupt partition tables, unusual layouts). diff --git a/.changeset/device-aware-doctor.md b/.changeset/device-aware-doctor.md deleted file mode 100644 index b0254bc8..00000000 --- a/.changeset/device-aware-doctor.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@podkit/core': minor -'podkit': minor ---- - -Add device-aware diagnostics framework to `podkit doctor`. The doctor command now handles mass-storage devices gracefully instead of crashing when pointed at a non-iPod device. Diagnostic checks declare which device types they apply to, and the runner filters them automatically. JSON output now includes a `deviceType` field. diff --git a/.changeset/device-capability-finalisation.md b/.changeset/device-capability-finalisation.md deleted file mode 100644 index b89baf87..00000000 --- a/.changeset/device-capability-finalisation.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'podkit': patch -'@podkit/core': minor -'@podkit/ipod-firmware': minor -'@podkit/device-types': minor -'@podkit/devices-ipod': patch -'@podkit/devices-mass-storage': patch ---- - -P4 — device-capability architecture complete. All in-tree migration finished; deprecated shims removed. - -**Breaking changes in `@podkit/core`:** The following symbols have been removed from the public API: `createIpodCapabilities`, `LibgpodDeviceInfo`, `DEVICE_PRESETS`, `DevicePreset`, `getDevicePreset`, and `resolveDeviceCapabilities`. Callers must migrate before upgrading. - -Migration guide: -- `createIpodCapabilities(libgpodInfo)` → `resolveCapabilities(identity)` (preferred, identity-driven) or `resolveIpodModelCapabilities(modelFromLibgpodInfo(libgpodInfo))` for callers that genuinely hold libgpod data. -- `getDevicePreset(deviceType)` → `BUILT_IN_PRESETS[deviceType]` from `@podkit/devices-mass-storage`. -- `DEVICE_PRESETS` → `BUILT_IN_PRESETS` from `@podkit/devices-mass-storage`. -- `resolveDeviceCapabilities(type, overrides)` → `resolveCapabilities(identity, { overrides })` from `@podkit/core`. -- The `core/device/sysinfo-extended` shim path is gone; import `readSysInfoExtended`, `writeSysInfoExtended`, and `ensureSysInfoExtended` from `@podkit/ipod-firmware` directly. - -See ADR-295.07 for the full architectural rationale. - -**New in `@podkit/ipod-firmware`:** SysInfoExtended file I/O is now owned by this package: `readSysInfoExtended`, `writeSysInfoExtended`, `ensureSysInfoExtended`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR`. Diagnostic helpers `compareSysInfoConsistency` and `normaliseFireWireGuid` are also exported. `ParsedFirmware` gains the optional `modelNumber` field (populated from the `ModelNumStr` plist key when present). - -**New in `@podkit/device-types`:** `IpodModel`, `IpodChecksumType`, `IpodGenerationId`, `IpodGenerationIdLike`, `IpodModelSource`, and `IPOD_GENERATION_IDS` are now exported from this package (canonical home). `UsbConnectionInfo` has been removed — use `UsbFingerprint` instead. `IpodIdentity.notSupportedReason` is added for devices identified as iPods that podkit cannot fully support (e.g. iOS-mode devices). `DeviceCapabilities.artworkMaxResolution` is now `number | null` (null when the generation has no known limit or artwork is unsupported). - -**In `@podkit/devices-ipod`:** `IpodGeneration.supported` and `IpodGeneration.artworkMaxResolution: number | null` are new fields on every generation entry. `lookupByFamilyId` and `FAMILY_ID_TO_GENERATION` are now exported. `unsupported.ts` is populated with comprehensive Apple iOS device PIDs to allow early rejection of phones and tablets at the USB identification stage. - -No user-facing CLI behaviour changes. `podkit device scan`, `podkit device info`, and all sync paths behave identically to P3. diff --git a/.changeset/device-commands-readiness-output.md b/.changeset/device-commands-readiness-output.md deleted file mode 100644 index 6958a7e4..00000000 --- a/.changeset/device-commands-readiness-output.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"podkit": minor ---- - -Enhanced device commands with readiness diagnostics - -- `device scan`: verbose readiness output with per-stage checks, USB discovery for unpartitioned devices, config relationship display, `--mount` flag for automatic mounting, `--report` flag for diagnostic reports -- `podkit doctor`: two-phase diagnostics — readiness checks before database health, graceful handling of devices without databases -- `device info`: readiness summary line in output -- `device init`: readiness-aware guidance with stub messages for format/partition operations -- OS error codes (errno 71, 13, 19, 5) translated to plain-language explanations diff --git a/.changeset/device-config-unsupported-rich-shape.md b/.changeset/device-config-unsupported-rich-shape.md deleted file mode 100644 index 0eddf650..00000000 --- a/.changeset/device-config-unsupported-rich-shape.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"podkit": minor ---- - -`DeviceConfig.unsupported` (the marker for devices the user added via the warn-allow flow in TASK-317.03) is now a structured object (`{ kind, confirmedAt }`) instead of a bare boolean. The `kind` captures which unsupported-reason class triggered the prompt (iOS device, hashAB nano, mass-storage with no preset, etc.) so a future reader can tell why the device was confirmed. The `confirmedAt` ISO timestamp records when. Legacy `unsupported = true` config entries are silently coerced to the new shape on load. diff --git a/.changeset/device-info-firmware-vs-operational-codecs.md b/.changeset/device-info-firmware-vs-operational-codecs.md deleted file mode 100644 index eff89843..00000000 --- a/.changeset/device-info-firmware-vs-operational-codecs.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"podkit": minor ---- - -`podkit device info` now distinguishes the "device firmware can play" codec list from the "podkit will write" list on mass-storage devices. When the two disagree (e.g. rockbox declares `wav`/`aiff` but podkit transcodes them before transfer), the capabilities block expands into a `Firmware:` / `Podkit:` sub-block with the gap codecs annotated as transcoded: - -``` -Capabilities: - Audio Codecs: - Firmware: aac, alac, mp3, flac, vorbis, opus, wav, aiff - Podkit: aac, alac, mp3, flac, vorbis, opus - (wav, aiff transcoded before transfer) -``` - -When the two lists agree (echo-mini, generic), the existing single `Audio Codecs:` line is preserved. JSON output gains a `status.massStorageCapabilities.firmwareSupportedAudioCodecs` field, omitted when there is no diff (absence signals the two views are equal). iPod output is unchanged. diff --git a/.changeset/device-list-resolved-config.md b/.changeset/device-list-resolved-config.md deleted file mode 100644 index 23892182..00000000 --- a/.changeset/device-list-resolved-config.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"podkit": minor ---- - -Redesigned `podkit device list` output with resolved config values and provenance tracking - -- Shows resolved quality, audio, video, and artwork settings per device with inheritance indicators -- Global config line shows top-level resolved values -- Connected devices detected automatically and marked with ● prefix -- Devices sorted by connection status, then default, then alphabetical -- Values explicitly set on a device shown without brackets; inherited values wrapped in [brackets] -- Unsupported capabilities shown as ✗, unknown (disconnected iPod) shown as ? -- TYPE column replaces VOLUME column -- New config resolution module (`config/resolve.ts`) with `ResolvedValue` provenance tracking -- `device scan` "Configured devices" section renamed to "Not detected" and now includes iPod devices diff --git a/.changeset/device-output-refactor.md b/.changeset/device-output-refactor.md deleted file mode 100644 index b169a3e4..00000000 --- a/.changeset/device-output-refactor.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"podkit": minor -"@podkit/core": patch ---- - -Improve device command output: USB model in scan, SysInfo mismatch detection, summary/issues layout - -- `podkit device scan` now shows the USB-detected iPod model (e.g., "iPod Classic 6th generation (USB)") and always runs USB discovery in parallel with disk scanning -- `podkit device scan` and `podkit doctor` detect generation mismatches between SysInfo and USB data, warning when the SysInfo file may have been copied from a different device -- `podkit device info`, `podkit device scan`, and `podkit doctor` now separate compact check summaries from detailed issue explanations — warnings and fix commands appear in a dedicated "Issues" section instead of inline -- New `lookupGenerationByModelNumber()` function in `@podkit/core` for resolving iPod generation from SysInfo model numbers diff --git a/.changeset/device-readiness-diagnostics.md b/.changeset/device-readiness-diagnostics.md deleted file mode 100644 index 30485cb3..00000000 --- a/.changeset/device-readiness-diagnostics.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@podkit/core": minor ---- - -Add device readiness diagnostic system - -- New 6-stage readiness pipeline (USB → Partition → Filesystem → Mount → SysInfo → Database) that checks every stage of device health -- OS error code interpreter translates errno values into actionable explanations -- USB discovery finds iPods even without disk representation (unpartitioned/uninitialized devices) -- Enhanced SysInfo validation detects missing, corrupt, or unrecognized model files -- Diagnostics framework now handles missing database gracefully (checks skip instead of crashing) diff --git a/.changeset/device-scan-unsupported-reason.md b/.changeset/device-scan-unsupported-reason.md deleted file mode 100644 index 98480963..00000000 --- a/.changeset/device-scan-unsupported-reason.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"podkit": minor ---- - -`podkit device scan --format json`: rename `notSupportedReason: string` to `unsupportedReason: ReadinessUnsupportedReason` on USB-only device entries - -The JSON envelope for `device scan` previously carried unsupported-device -diagnostics as a bare `notSupportedReason` string. It now matches the structured -`ReadinessUnsupportedReason` shape already used by the readiness pipeline and -`IpodModel.unsupportedReason`: - -```json -{ - "unsupportedReason": { - "kind": "ios-device", - "headline": "iPod Touch is not supported by podkit.", - "docsUrl": "https://jvgomg.github.io/podkit/devices/supported-devices/" - } -} -``` - -Consumers reading `device.notSupportedReason` should read -`device.unsupportedReason.headline` instead — the same string, just nested -under the typed payload. The change applies to both USB-only iPod entries -(touch, iPhone, iPad, nano 6G/7G, shuffle 3G/4G) and to vendor-recognised -mass-storage devices with no matching preset. - -The same rename also lands on the internal `IpodIdentity` and -`IpodClassification` shapes, but those are not part of the public CLI surface. diff --git a/.changeset/device-track-artwork-move-to-adapter.md b/.changeset/device-track-artwork-move-to-adapter.md deleted file mode 100644 index 31673ae8..00000000 --- a/.changeset/device-track-artwork-move-to-adapter.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"@podkit/core": minor ---- - -Move artwork operations from `DeviceTrack` onto `DeviceAdapter` (Option Z) - -Public API: the `DeviceTrack` and `IpodTrack` interfaces no longer expose -`setArtwork(path)`, `setArtworkFromData(bytes)`, or `removeArtwork()`. The -`DeviceAdapter` interface gains: - -- `setTrackArtwork(track, imageData): Promise` — write artwork bytes; - adapter dispatches internally on `track.artworkSink` (iPod ArtworkDB, - mass-storage embedded tag, sidecar `cover.jpg`, or no-op). -- `removeTrackArtwork(track): Promise` (was `T`, optional) — clear - artwork. Now required on every adapter; iPod clears the ArtworkDB entry, - mass-storage is a deliberate no-op. - -The pipeline's `transferArtwork` is now a single `adapter.setTrackArtwork` -call instead of a three-way branch on `artworkSink`. `artworkSink` remains -on `DeviceTrack` as a readable introspection property used for progress -reporting and to suppress dishonest `syncTag.artworkHash` claims on noop -adapters (churn-loop guard, doc-041 §3.6). Behaviour is unchanged: iPod -bytes go to libgpod's ArtworkDB, mass-storage bytes route through the tag -writer or sidecar cover.jpg path. Internal consumers (Pipeline, repair, -diagnostics) are updated; downstream callers using `DeviceAdapter` need -only switch to the adapter methods. diff --git a/.changeset/devices-extraction-and-providers.md b/.changeset/devices-extraction-and-providers.md deleted file mode 100644 index e9663c67..00000000 --- a/.changeset/devices-extraction-and-providers.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'podkit': minor -'@podkit/core': minor -'@podkit/devices-ipod': patch -'@podkit/devices-mass-storage': patch ---- - -New packages: `@podkit/devices-ipod` (canonical home for iPod generation tables, model lookups, and capability synthesis) and `@podkit/devices-mass-storage` (user-extensible DAP preset framework for Echo Mini, Rockbox, generic, and custom devices). - -Echo Mini is now auto-detected at `device add` — when the USB descriptor matches the known VID/PID (`0x071b`/`0x3203`), no `--type echo-mini` flag is required. - -`enumerateConnectedDevices` is now the recommended way to discover and classify USB devices. It accepts a `providers: DeviceProvider[]` array and returns `EnumeratedDevice[]` carrying both the USB connection info and the provider-produced identity. - -`getCapabilities` in `@podkit/devices-ipod` is libgpod-free. Capability synthesis is purely table-and-firmware-driven; the legacy `createIpodCapabilities` adapter that depended on a live libgpod `LibgpodDeviceInfo` struct is deprecated in `@podkit/core`. Parity is verified across all 29 generations (the 4 that were libgpod `unknown` degenerate cases are now correctly populated from the table). - -Internal re-export shims in `@podkit/core` keep all existing call paths compiling for one release. The shims delegate to `@podkit/devices-ipod` and `@podkit/devices-mass-storage` and will be removed in P4. diff --git a/.changeset/diagnostic-scope-three-way.md b/.changeset/diagnostic-scope-three-way.md deleted file mode 100644 index 2d61b1a8..00000000 --- a/.changeset/diagnostic-scope-three-way.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Refactor the diagnostic-check scope model from a 2-field shape (`scope: 'system' | 'device'` + `category?: 'readiness' | 'database'`) to a single required 3-way union (`scope: 'system' | 'device-readiness' | 'database-health'`). Compile-time enforcement that every check declares which section it renders into; no more silent fallback when `category` is missing. The user-facing CLI `--scope` flag values are unchanged. diff --git a/.changeset/doctor-artwork-reset-and-rebuild-rename.md b/.changeset/doctor-artwork-reset-and-rebuild-rename.md deleted file mode 100644 index b96a2b68..00000000 --- a/.changeset/doctor-artwork-reset-and-rebuild-rename.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Add `podkit doctor --repair artwork-reset` to clear all artwork from an iPod without needing a source collection. This is a fast alternative to a full rebuild — useful when your source collection isn't available or you just want to clear corrupted artwork quickly. - -Rename `--repair artwork-integrity` to `--repair artwork-rebuild` to better describe what the repair does. The old name no longer works. diff --git a/.changeset/doctor-consistent-sections.md b/.changeset/doctor-consistent-sections.md deleted file mode 100644 index 91b2aad6..00000000 --- a/.changeset/doctor-consistent-sections.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -`podkit doctor` now renders a consistent `System` / `Device Readiness` / `Database Health` section structure across all device types. Previously, mass-storage devices (Echo Mini) collapsed everything into a single `Device Health` bucket and mis-categorised three system-scope checks. The fix audits every check's `scope` tag, adds a `category?: 'readiness' | 'database'` discriminator so device-scope checks can be routed to the right subsection, and skips `iPod Firmware Inquiry Methods` on non-iPod devices. diff --git a/.changeset/doctor-no-system-and-sysinfo-redesign.md b/.changeset/doctor-no-system-and-sysinfo-redesign.md deleted file mode 100644 index ea36a84c..00000000 --- a/.changeset/doctor-no-system-and-sysinfo-redesign.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@podkit/core': minor -'podkit': minor ---- - -`podkit doctor` gains `--no-system` to skip system-scope checks (FFmpeg encoders, libusb availability, udev rule). System checks remain on by default; pass `--no-system` for device-only diagnostics or in tests where the host environment shouldn't influence the result. - -The `sysinfo-consistency` check is redesigned: a missing `SysInfoExtended` file is now `skip` (not `fail`) since absence is not a failure mode. When the file is present it's compared against the live device on two independent axes — FireWireGUID and model generation — and only fails when at least one axis can be evaluated and disagrees. The check picks up live device data via the new `liveIdentity` field on `DiagnosticContext`, which `runDiagnostics` accepts as part of `RunDiagnosticsInput`. diff --git a/.changeset/doctor-repair-correctness.md b/.changeset/doctor-repair-correctness.md deleted file mode 100644 index fe4bf48b..00000000 --- a/.changeset/doctor-repair-correctness.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"podkit": patch -"@podkit/core": patch -"@podkit/ipod-firmware": patch ---- - -Fix three `podkit doctor` repair correctness bugs: - -- `--repair sysinfo-consistency` now overwrites a stale on-disk SysInfoExtended (previously short-circuited on file existence, reporting success without rewriting). -- `--repair sysinfo-extended` no longer requires an existing iTunesDB — repairs without a `database` requirement skip the DB open so identity-populating repairs work on freshly formatted iPods. New `'database'` value on `RepairRequirement`. -- The readiness `SysInfoExtended:` status line distinguishes a missing file from a present-but-unparseable one. diff --git a/.changeset/doctor-repairs-acquire-sync-lock.md b/.changeset/doctor-repairs-acquire-sync-lock.md deleted file mode 100644 index 04a310b9..00000000 --- a/.changeset/doctor-repairs-acquire-sync-lock.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'podkit': minor -'@podkit/core': minor ---- - -`podkit doctor` repairs now acquire the per-device sync lock before mutating the device. - -Previously, `podkit doctor --repair ` would happily run while a `podkit sync` (or daemon-driven sync) was mid-flight against the same device. For mass-storage devices, this meant doctor's `--repair orphan-files` could prune phantom manifest entries from `state.json`, only for sync's eventual `save()` to clobber the prune from in-memory state — silently undoing the user's repair. For iPod devices, concurrent libgpod writes (artwork rebuilds, sysinfo fixes, debris cleanup) could corrupt the iTunesDB. - -The fix: every `--repair` that mutates the device now acquires the same per-device lock that `podkit sync` takes (`.podkit/sync.lock` for mass-storage, `iPod_Control/.podkit-sync.lock` for iPod). On contention, doctor exits with `LOCK_HELD` (exit code **4**) and a message naming the holding PID: - -``` -Error: Another podkit process is using /Volumes/TERAPOD (pid 12345). Wait for it to finish or kill it. -``` - -Audited and locked: `orphan-files`, `artwork-rebuild`, `artwork-reset`, `debris-files` (iPod), `sysinfo-extended`, `sysinfo-consistency`, `sysinfo-modelnum-mismatch`. System-only repairs that don't touch the device (`udev-rule`, `debris-transcode-tmp`) correctly skip the lock. `--dry-run` repair invocations also skip the lock — dry-run is read-only by design and matches `podkit sync --dry-run`'s policy. - -**Internal:** `resolveSyncLockPath` moved from the CLI to `@podkit/core` (exported from `lib/sync-lock-path.ts`) so doctor and sync share the same implementation. New JSDoc on `pruneManifestRows` documents the lock requirement for any future direct caller. Architecture doc `documents/architecture/sync/planning.md` §6 now enumerates every manifest-writer surface with confirmed lock semantics. diff --git a/.changeset/doctor-scope-system.md b/.changeset/doctor-scope-system.md deleted file mode 100644 index f269e8b0..00000000 --- a/.changeset/doctor-scope-system.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"podkit": minor ---- - -Add `podkit doctor --scope ` for running host-environment checks without a registered device. - -`--scope system` skips device resolution entirely and runs only the system-scope checks (FFmpeg, codec encoders, video encoder, libgpod runtime, SCSI inquiry, udev rule on Linux). Useful before plugging an iPod in for the first time, and required by the m-19 Tier-3 test harness to assert host-state against a captured `SystemState` fixture. - -`--scope device` requires `-d/--device` and runs only device-scope checks. `--scope all` (default) preserves the existing combined output byte-for-byte; the legacy `--no-system` flag still applies in that mode. JSON output under `--scope system` uses a discriminator field (`scope: "system"`) so consumers can distinguish the two envelopes. diff --git a/.changeset/doctor-source-validity-probe.md b/.changeset/doctor-source-validity-probe.md deleted file mode 100644 index 07170ca9..00000000 --- a/.changeset/doctor-source-validity-probe.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"podkit": patch -"@podkit/core": patch ---- - -`podkit doctor` (artwork-rebuild repair) now runs a per-track source-file validity probe (stat + 16-byte magic-byte header check) before the album-cache lookup. Corrupt or unreadable source files always land in the `errors` bucket with a structured reason (`missing | unreadable | truncated | badMagic`) rather than inheriting a sibling track's cache success non-deterministically. - -The `details.errorDetails[*]` in doctor's JSON output now carries optional `path` and `reason` fields so users can act on specific bad files. Backward-compatible: existing `artist` / `title` / `error` fields are preserved. - -Magic-byte signatures cover FLAC, OGG/Opus, MP3 (ID3 and bare MPEG sync), MP4/M4A/AAC, WAV, AIFF/AIFC — matching the directory adapter's accepted extensions. diff --git a/.changeset/doctor-unified-repair-ids.md b/.changeset/doctor-unified-repair-ids.md deleted file mode 100644 index 544b55fa..00000000 --- a/.changeset/doctor-unified-repair-ids.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'podkit': minor -'@podkit/core': minor ---- - -Unify `--repair` IDs across device types and add debris-only diagnostic checks. - -The `podkit doctor --repair` flag now uses one ID per repair regardless of device type. Internally, the framework dispatches the right walker based on the connected device: - -- `--repair orphan-files` — works on both iPod and mass-storage. (Previously `orphan-files` was iPod-only; `orphan-files-mass-storage` was the mass-storage variant.) -- `--repair debris-files` (new) — cleans podkit's own `.podkit-tmp` and adapter-failure write residue from prior interrupted syncs. Repair is safe-by-design (no confirmation prompt) because every debris file is incomplete by construction. -- `--repair debris-transcode-tmp` (new) — reaps abandoned `podkit-transcode-*` scratch directories from SIGKILLed prior syncs. Uses an mtime-based safety floor so concurrent sibling processes are never disturbed. - -**Breaking:** `--repair orphan-files-mass-storage` has been **removed**. Users running this flag will see Commander's choices() validation error listing the new public IDs (including `orphan-files`). Migration is mechanical: replace every occurrence with `--repair orphan-files`. - -The orphan check no longer reports debris in its detail output — that's the new `debris-files` check's job. Same FS walk, two checks; no double traversal. diff --git a/.changeset/echo-mini-e2e-bugfixes.md b/.changeset/echo-mini-e2e-bugfixes.md deleted file mode 100644 index 49b9ab4b..00000000 --- a/.changeset/echo-mini-e2e-bugfixes.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -"@podkit/core": patch -"podkit": patch ---- - -Fix multiple bugs discovered during end-to-end Echo Mini hardware validation - -**Sync pipeline:** - -- Create temp directory for optimized-copy operations (not just transcodes), fixing "No such file or directory" FFmpeg failures on mass-storage devices -- Capture last 1000 chars of FFmpeg stderr (instead of first 500) so actual errors aren't swallowed by the version banner - -**Device preset content paths:** - -- Pass device preset content paths to adapter even when no user overrides exist, fixing Echo Mini's `musicDir: ''` being ignored and files landing in `Music/` instead of device root - -**Artwork:** - -- Read embedded artwork during mass-storage device scan (`skipCovers: false`) so artwork presence is correctly detected, preventing false `artwork-added` upgrades on every sync -- Force `yuvj420p` (4:2:0) pixel format in artwork scale filter — JPEG with 4:4:4 chroma subsampling does not display on the Echo Mini - -**Sync tag and preset detection:** - -- Treat `quality=copy` sync tags as in-sync when the classifier would also route the source as a copy, preventing false preset-upgrade detection on FLAC-capable mass-storage devices -- Route lossless sources to transcode (not copy) when quality preset is non-lossless, even if the device natively supports the source codec (e.g., FLAC device with quality=high should produce AAC) diff --git a/.changeset/eject-device-label.md b/.changeset/eject-device-label.md deleted file mode 100644 index 3771a67e..00000000 --- a/.changeset/eject-device-label.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@podkit/core": patch -"podkit": patch ---- - -Use configurable device label in eject messages instead of hardcoded 'iPod' diff --git a/.changeset/fields-validation-error.md b/.changeset/fields-validation-error.md deleted file mode 100644 index 8dd4bba3..00000000 --- a/.changeset/fields-validation-error.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"podkit": patch ---- - -Hard error on invalid `--fields` names with message listing valid fields; valid fields now listed in `--help` diff --git a/.changeset/fix-device-model-data.md b/.changeset/fix-device-model-data.md deleted file mode 100644 index 61889722..00000000 --- a/.changeset/fix-device-model-data.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@podkit/core": patch ---- - -Fix incorrect device model data: correct checksum types (nano 7G: hashAB, touch 1G-3G: hash72), fix USB product IDs (0x1205: iPod mini, 0x1209: iPod Video, 0x120a: nano 1G), reclassify model B867 from nano 4G to shuffle 3G, add 15 nano 7G model number variants, add missing touch 4G serial suffixes, and add first crowd-sourced nano 7G serial suffix mapping diff --git a/.changeset/fix-process-exit-stdout-truncation.md b/.changeset/fix-process-exit-stdout-truncation.md deleted file mode 100644 index 329af4e6..00000000 --- a/.changeset/fix-process-exit-stdout-truncation.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"podkit": patch ---- - -Fix stdout truncation when piping CLI output to another process. Commands that used `process.exit(1)` could terminate before stdout buffers flushed, truncating JSON output (e.g. `podkit init --json | node -e ...`). All error exit paths now use `process.exitCode = 1` and return normally, allowing Node.js to drain streams before exiting. diff --git a/.changeset/fix-subsonic-source-display.md b/.changeset/fix-subsonic-source-display.md deleted file mode 100644 index 0d31c06f..00000000 --- a/.changeset/fix-subsonic-source-display.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"podkit": patch ---- - -Fix blank source path in sync output for subsonic collections diff --git a/.changeset/fix-sudo-config-path.md b/.changeset/fix-sudo-config-path.md deleted file mode 100644 index 569c0ecb..00000000 --- a/.changeset/fix-sudo-config-path.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'podkit': patch ---- - -Fix config not found when running `podkit` under `sudo`. The default config path now resolves the invoking user's home directory via `SUDO_USER`/`DOAS_USER` and `/etc/passwd`, rather than using root's home. diff --git a/.changeset/identity-cascade-compose.md b/.changeset/identity-cascade-compose.md deleted file mode 100644 index 58db8422..00000000 --- a/.changeset/identity-cascade-compose.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"podkit": patch -"@podkit/core": patch -"@podkit/ipod-firmware": patch -"@podkit/devices-ipod": patch ---- - -Fix iPod model identification regressing to "Unknown iPod" after `doctor --repair sysinfo-extended` on pre-2006 devices (mini 2G), and tighten the package-boundary contract so consumers compose identity instead of injecting resolution policy. - -The bug: each consumer of `ensureSysInfoExtended` / `readSysInfoExtended` passed a serial-only `resolveModel` callback. When the 3-character serial suffix wasn't in `tables/serials.ts`, the resolver returned undefined and the device was displayed as "Unknown iPod" — even when a SysInfo file with a known `ModelNumStr` was sitting next to the SysInfoExtended on disk. - -The fix: - -- **Removed** `ModelResolver` type and the `resolveModel` callback from `@podkit/ipod-firmware`. `readSysInfoExtended` and `ensureSysInfoExtended` now return a flat `SysInfoIdentity` bag (`firewireGuid?, serialNumber?, modelNumStr?, familyId?`). When a SysInfo file is on disk alongside SysInfoExtended, its `ModelNumStr` is read opportunistically. -- **Callers compose** with `resolveIpodModel(bag)` from `@podkit/devices-ipod`, which cascades modelNumStr → serial → productId → familyId → libgpodGeneration. The CLI no longer makes resolution decisions. -- **Added** `SYSINFO_PATH`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR` exported from `@podkit/ipod-firmware` and re-exported from `@podkit/core`. Consumers use these constants instead of duplicating the literal `iPod_Control/Device/...` paths. -- **Added** `S4G: '9804'` entry to `tables/serials.ts` (mini 2G 4GB Pink, sourced from real hardware, serial `JQ5141TFS4G`). -- **Post-write enrichment.** After `ensureSysInfoExtended` writes the file via USB inquiry, it now re-reads via `readSysInfoExtended` so the post-write identity bag includes `modelNumStr` from the SysInfo neighbour. Eliminates the cosmetic regression where the repair-success message showed a less-specific name than the subsequent `doctor` run. diff --git a/.changeset/inquiry-orchestrator-error-detail.md b/.changeset/inquiry-orchestrator-error-detail.md deleted file mode 100644 index 2684867f..00000000 --- a/.changeset/inquiry-orchestrator-error-detail.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"podkit": patch -"@podkit/ipod-firmware": patch ---- - -Improve the firmware-inquiry orchestrator's failure message so users can see what went wrong without `-vv`. The default error now names every transport attempted (USB, SCSI) with each one's failure reason on its own line and includes a remediation hint (e.g. `podkit doctor --repair udev-rule` for EACCES on `/dev/sg*` or `/dev/bus/usb/...`). The orchestrator also no longer short-circuits a planned SCSI fallback when USB hits a permission wall — both transports run if the plan calls for it. diff --git a/.changeset/ipod-firmware-scsi-delivery.md b/.changeset/ipod-firmware-scsi-delivery.md deleted file mode 100644 index a820035b..00000000 --- a/.changeset/ipod-firmware-scsi-delivery.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor -"@podkit/device-types": minor -"@podkit/ipod-firmware": minor ---- - -Add SCSI firmware inquiry for iPod identification (P1 — m-18 device-capability architecture). - -`@podkit/device-types` (first published release) provides the canonical shared type definitions — `DeviceCapabilities`, `DeviceIdentity`, `ParsedFirmware`, and `DeviceProvider` — used across the podkit monorepo without circular dependencies. - -`@podkit/ipod-firmware` (first published release) implements iPod firmware inquiry via SCSI (Linux SG_IO + macOS IOKit, using koffi FFI) with USB fallback through the existing libgpod-node binding. Devices that previously failed identification over USB — including iPod mini 2G, nano 2G, and some iPod 5G Video configurations — can now be identified via SCSI. The orchestrator probes available transports at startup, prefers USB when both are available, and falls back to SCSI transparently. - -`@podkit/core` now routes `ensureSysInfoExtended` through the new orchestrator with SCSI fallback, and registers two new `podkit doctor` checks: `inquiry-methods` (reports which transports are available on this host) and `sysinfo-consistency` (validates that the on-disk SysInfo file matches the live firmware read). EACCES errors from SCSI include step-by-step recovery instructions. - -`podkit` CLI gains `--repair udev-rule` in `podkit doctor` to install the Linux udev rule that grants non-root `/dev/sg*` access, and surfaces the new doctor checks in the readiness output. diff --git a/.changeset/ipod-model-identity.md b/.changeset/ipod-model-identity.md deleted file mode 100644 index ec7e56b4..00000000 --- a/.changeset/ipod-model-identity.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@podkit/core": minor -"podkit": minor ---- - -Add canonical IpodModel type for structured device identity - -- Add `IpodModel` interface — canonical representation of identified iPod model with `displayName`, `generationId`, `checksumType`, `color`, `capacityGb`, `modelNumber`, and `source` provenance -- Add `resolveIpodModel()` factory — builds an `IpodModel` from USB product ID, SysInfo model number, or serial number suffix -- Add `UsbConnectionInfo` interface — pure USB bus topology data, split from device identity -- Restructure `UsbDiscoveredDevice` to carry `usb: UsbConnectionInfo` + `model?: IpodModel` -- Add `usbModel` and `deviceModel` to `ReadinessResult` — USB-derived and SysInfo-derived models kept separate for mismatch detection -- Update `SysInfoExtendedResult` with structured `model`, `firewireGuid`, `serialNumber` fields -- Clean `checkSysInfo()` return type — new `SysInfoCheckResult` separates stage result from device model -- Add `model` to JSON output for `device scan` and `device info` commands -- `device scan` and `device info` now display richest available model name (color/capacity from SysInfo when available) -- Remove `UsbDeviceInfo` type (replaced by `UsbConnectionInfo` + `IpodModel`) diff --git a/.changeset/libgpod-node-device-class.md b/.changeset/libgpod-node-device-class.md deleted file mode 100644 index b7588ce7..00000000 --- a/.changeset/libgpod-node-device-class.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@podkit/libgpod-node": minor ---- - -Add standalone Device class for capability queries without opening a database - -- `Device.fromMountPoint(path)` — reads SysInfo from filesystem, determines capabilities -- `Device.fromModelNumber(num)` — cached lookup from model number string, no filesystem needed -- Exposes `supportsArtwork`, `supportsVideo`, `supportsPhoto`, `supportsPodcast`, `generation`, `modelNumber`, `modelName`, `capacity` -- Add `ArtworkFormat` type (reserved for future artwork dimension exposure) diff --git a/.changeset/macos-regression-coverage.md b/.changeset/macos-regression-coverage.md deleted file mode 100644 index a7c933c3..00000000 --- a/.changeset/macos-regression-coverage.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@podkit/core': patch -'podkit': patch -'@podkit/device-testing': patch ---- - -macOS regression coverage for the m-18 TASK-317.* hygiene cluster. Extends the test surface so the macOS-platform code paths that ship today (HFS+ supported, `system_profiler` `bsd_name` partition-suffix handling, `sysinfo-modelnum-mismatch` diagnostic framework, unsupported-cascade suppression, doctor section ordering and visibility, JSON envelope shape, TOML round-trip) all have explicit pinned assertions. - -Foundation: `DevicePersona` now carries an optional `platformDeviceInfoDarwin?: PlatformDeviceInfo[] | null` field, and `ipodMacosPlatformInfo(opts)` in `@podkit/device-testing/personas/builders` synthesises canonical macOS-shape records. Populated on `ipodMini2gPink` (FAT32) and `ipodNano4gHfsplus` (HFS+) as representative fixtures. diff --git a/.changeset/mass-storage-delete-and-orphans.md b/.changeset/mass-storage-delete-and-orphans.md deleted file mode 100644 index e2978bd9..00000000 --- a/.changeset/mass-storage-delete-and-orphans.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Fix `--delete` to only remove managed files on mass-storage devices, and add orphan file detection via `podkit doctor`. - -**Bug fix:** `--delete` previously removed all unmatched files on mass-storage devices, including user-placed files. It now only removes files that podkit manages (tracked in `.podkit/state.json`), matching iPod behavior where only database tracks are candidates for deletion. - -**Collision detection:** Sync now detects when a planned file write would collide with an existing unmanaged file and reports the conflict before writing. Works in both normal sync and `--dry-run` mode. - -**New diagnostic check:** `podkit doctor` now runs health checks on mass-storage devices. The `orphan-files-mass-storage` check detects unmanaged files in content directories and can clean them up via `podkit doctor --repair orphan-files-mass-storage`. - -**Other improvements:** -- State manifest (`.podkit/state.json`) is now written without pretty-printing to reduce file size on device storage -- Shell completions now include valid repair IDs for the `--repair` option diff --git a/.changeset/mass-storage-device-support.md b/.changeset/mass-storage-device-support.md deleted file mode 100644 index f1d1b335..00000000 --- a/.changeset/mass-storage-device-support.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor -"@podkit/daemon": minor ---- - -Add mass-storage device support for non-iPod portable music players. - -**Supported device types:** Echo Mini, Rockbox, and generic mass-storage DAPs. iPod support is unchanged. - -**New in CLI (`podkit`):** -- `podkit device add --type ` registers mass-storage devices by type and mount path -- `podkit device info/music/video` work with mass-storage devices via `DeviceAdapter` interface -- `podkit device scan` shows configured path-based devices alongside auto-detected iPods -- `podkit sync` routes to the correct adapter (iPod or mass-storage) based on device config -- Video sync now uses capabilities-based gating instead of iPod-only checks -- Safety gates on `device init/reset/clear` (iPod-only commands) for mass-storage devices -- Mount and eject commands show device-appropriate messaging -- Config validation rejects capability overrides on iPod devices (capabilities are auto-detected from generation) -- Shared `openDevice()` function eliminates duplicated device-opening logic across commands - -**New in core (`@podkit/core`):** -- `DeviceAdapter` interface — generic abstraction over device databases (iPod, mass-storage) -- `MassStorageAdapter` — filesystem-based track management with `.podkit/state.json` manifest -- `IpodDeviceAdapter` — thin wrapper making `IpodDatabase` implement `DeviceAdapter` -- Device capability presets for Echo Mini, Rockbox, and generic devices -- `resolveDeviceCapabilities()` merges preset defaults with user config overrides -- `DeviceTrack` type used throughout sync engine (replaces `IPodTrack` casts in execution paths) -- Configurable content path prefixes (`musicDir`, `moviesDir`, `tvShowsDir`) with device-type defaults -- Device presets include default content paths (Echo Mini: root for music; generic/Rockbox: `Music/`, `Video/Movies/`, `Video/Shows/`) -- Manifest v2 stores active content paths; files automatically moved when prefixes change -- Root path support (`/`, `.`, or empty string all normalize to device root) -- Content path duplicate validation (no two content types can share the same prefix) -- Video scanning support for mass-storage devices (.m4v, .mp4, .mov, .avi, .mkv) - -**New in daemon (`@podkit/daemon`):** -- Mass-storage device polling via `PODKIT_MASS_STORAGE_PATHS` env var (colon/comma separated) -- Second `DevicePoller` + `SyncOrchestrator` pair for mass-storage devices -- No-op mount/eject runners (mass-storage devices are externally managed) -- Graceful shutdown handles both iPod and mass-storage sync pipelines - -**Configuration:** -```toml -[devices.echo] -type = "echo-mini" -path = "/Volumes/ECHO" - -# Optional capability overrides (mass-storage only) -artworkMaxResolution = 800 -supportedAudioCodecs = ["aac", "mp3", "flac"] - -# Optional content path overrides (mass-storage only) -musicDir = "/" # Place music at device root -moviesDir = "Films" # Custom movies directory -tvShowsDir = "TV Shows" # Custom TV shows directory -``` - -**Environment variables for content paths:** -- `PODKIT_MUSIC_DIR` — global default music directory -- `PODKIT_MOVIES_DIR` — global default movies directory -- `PODKIT_TV_SHOWS_DIR` — global default TV shows directory diff --git a/.changeset/multi-ipod-daemon.md b/.changeset/multi-ipod-daemon.md deleted file mode 100644 index 31dcf60d..00000000 --- a/.changeset/multi-ipod-daemon.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@podkit/daemon": minor ---- - -Support multiple iPods plugged in simultaneously. Each device gets a unique mount point and devices appearing during a sync are queued and synced sequentially after the current sync completes. diff --git a/.changeset/ogg-artwork-embedding.md b/.changeset/ogg-artwork-embedding.md deleted file mode 100644 index 13153894..00000000 --- a/.changeset/ogg-artwork-embedding.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@podkit/core": minor ---- - -Embed artwork in OGG/Opus files for mass-storage devices with `artworkSources: ['embedded']`. - -FFmpeg's OGG muxer cannot write image streams (upstream tickets #4448, #9044, open since 2015), so OGG output was previously stripped of artwork with `-vn`. Mass-storage devices relying on embedded artwork showed no cover art for Opus tracks. - -**What changed:** - -- After FFmpeg produces an OGG file (with artwork stripped), the pipeline post-processes it via node-taglib-sharp to embed artwork as a `METADATA_BLOCK_PICTURE` Vorbis comment -- Artwork is resized to the device's `artworkMaxResolution` before embedding, matching the behavior of other formats where FFmpeg handles resize during transcode -- Resize results are cached per-album to avoid redundant FFmpeg image-processing spawns -- New `TagWriter.writePicture()` method and `resizeArtwork()` utility -- Pending picture writes follow the same deferred flush pattern as comment and ReplayGain tag writes (queued by `updateTrack`, flushed by `save()`) diff --git a/.changeset/path-template-cli-exposure.md b/.changeset/path-template-cli-exposure.md deleted file mode 100644 index 22f77c8d..00000000 --- a/.changeset/path-template-cli-exposure.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"podkit": minor ---- - -Expose `pathTemplate` as a per-device config option for mass-storage devices, allowing user-customisable folder structures. - -Configurable via: - -- `[devices.] pathTemplate = "..."` in TOML -- `PODKIT_PATH_TEMPLATE` env var (applied as a global default for mass-storage devices) - -Variables: `{albumArtist}`, `{artist}`, `{album}`, `{title}`, `{trackNumber}`, `{discNumber}`, `{totalDiscs}`, `{genre}`, `{year}`, `{ext}`. The template must contain `{title}` and `{ext}` and is rejected on iPod devices (iPod paths are managed by libgpod, not by template). - -Changing the template between syncs triggers the existing self-healing relocate flow — existing files are moved via `fs.rename()` to match the new layout, with no re-transcoding. Adds, removes (`--delete`), and template-driven relocates all compose in a single sync operation. diff --git a/.changeset/quality-upgrade-infinite-loop-fix.md b/.changeset/quality-upgrade-infinite-loop-fix.md deleted file mode 100644 index b80b7333..00000000 --- a/.changeset/quality-upgrade-infinite-loop-fix.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"podkit": patch -"@podkit/core": patch ---- - -Fixes an infinite loop in the music sync quality-upgrade path. When a track was upgraded via direct copy (no transcode) — for example when the source bitrate increased after a re-rip — `transferUpgradeToIpod` wrote the post-encode bitrate from the preparer, which is undefined for direct copies. The file was replaced on the device but the iPod-side bitrate stayed at the previous value, so the next sync detected the same upgrade again and re-fired it indefinitely. - -The fix resolves the post-upgrade bitrate as `prepared.bitrate ?? source.bitrate` so direct-copy upgrades carry the source bitrate through. The upgrade now converges on a single sync. - -Adds a `--force-sync-tags` bitrate backfill pass for pre-existing copied tracks whose iPod-side bitrate is 0 — symmetric with the existing artwork-hash baseline backfill. New users get correct bitrate tracking on first sync; existing users opt in with `--force-sync-tags`. - -Documents the upgrade-path semantics (format-upgrade gate, quality-upgrade gate, baseline write + backfill) in `documents/architecture/sync/upgrades.md`. diff --git a/.changeset/reconcile-discovery.md b/.changeset/reconcile-discovery.md deleted file mode 100644 index 63c9baa3..00000000 --- a/.changeset/reconcile-discovery.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Reconcile USB-inquiry and block-device discovery so each connected iPod renders once in `podkit device scan`. Previously, `device scan` could surface the same physical iPod twice on Linux when both pipelines independently identified it. The orphan entry also surfaced a destructive remediation (`Needs partitioning — see: podkit device init`) on a healthy device. Both issues fixed: a new reconciliation primitive matches USB and block-device records by serial number (or disk identifier as fallback), and the readiness-failure copy now points at docs instead of suggesting an inappropriate command. diff --git a/.changeset/refuse-hfsplus-on-linux.md b/.changeset/refuse-hfsplus-on-linux.md deleted file mode 100644 index 2ea396bc..00000000 --- a/.changeset/refuse-hfsplus-on-linux.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Refuse HFS+ iPods on Linux at `device add`; warn at `device scan` - -iPods formatted as HFS+ are now refused on Linux at `podkit device add` time, with a clear message pointing at docs explaining how to reformat to FAT32. `podkit device scan` surfaces the same iPods with a `Filesystem not supported on Linux` warning instead of running readiness stages or suggesting destructive remediation. macOS HFS+ behaviour is unchanged. - -Why: the Linux kernel hfsplus driver refuses RW on journaled HFS+ (the iPod default), udev/blkid don't surface a filesystem UUID for HFS+ on Linux (breaking podkit's identity model), and udisksctl mount paths fall back to a generic name with no label. Each friction point has a partial fix; together they mean Linux + HFS+ is a second-class experience no matter how much we patch. Refusing cleanly with a docs link sharpens podkit's Linux story to "FAT32 iPods, supported well." - -Structured `--json` output preserves a stable error code (`UNSUPPORTED_FILESYSTEM_ON_LINUX`) so scripted callers can handle the refusal. diff --git a/.changeset/remove-execute-music-plan.md b/.changeset/remove-execute-music-plan.md deleted file mode 100644 index 1d303832..00000000 --- a/.changeset/remove-execute-music-plan.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@podkit/core': minor ---- - -Remove `executeMusicPlan` library convenience. - -`executeMusicPlan` bypassed the engine `SyncExecutor` and carried subtly different save semantics (no checkpoint cadence) after ADR-019 P1 landed. Library callers should drive `MusicPipeline` through `createSyncExecutor(createMusicHandler(...))` for engine-owned save coordination, or instantiate `new MusicPipeline(deps)` and iterate `execute()` directly for minimal aggregation needs. diff --git a/.changeset/replaygain-mass-storage-tags.md b/.changeset/replaygain-mass-storage-tags.md deleted file mode 100644 index 94f35dba..00000000 --- a/.changeset/replaygain-mass-storage-tags.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@podkit/core": minor ---- - -Write ReplayGain tags to transcoded files for mass-storage devices with `audioNormalization: 'replaygain'` (e.g., Rockbox). - -Previously, ReplayGain data was only stored as iPod soundcheck values in the iTunes database. Mass-storage devices read volume normalization from file tags, but FLAC→AAC transcoding strips ReplayGain metadata. Tracks on Rockbox devices played without normalization. - -**What changed:** - -- ReplayGain tags (`REPLAYGAIN_TRACK_GAIN`, `REPLAYGAIN_TRACK_PEAK`) are injected via FFmpeg `-metadata` flags during transcoding for MP3, FLAC, and OGG/Opus output -- M4A files (where FFmpeg can't write ReplayGain metadata) get tags written via node-taglib-sharp after transfer -- Raw ReplayGain dB/peak values are preserved from collection sources (Subsonic API, local files) through the sync pipeline, avoiding precision loss from soundcheck integer conversion -- Device scan reads ReplayGain from file tags so the sync engine can detect when normalization data changes and needs updating -- Direct-copy operations skip tag writing since source files already have correct tags -- `soundcheckToReplayGainDb()` reverse conversion function added for back-converting when raw values aren't available - -**Bug fix:** `IpodTrackImpl` used `data.soundcheck || undefined` which coerced a valid soundcheck of `0` to `undefined`. Changed to `data.soundcheck ?? undefined`. diff --git a/.changeset/shorten-ipod-video-5-5g.md b/.changeset/shorten-ipod-video-5-5g.md deleted file mode 100644 index ac502403..00000000 --- a/.changeset/shorten-ipod-video-5-5g.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@podkit/core': patch ---- - -`shortenIpodLabel` (the short-label helper used in `device scan` / `device list` / `device info`) now compresses decimal-ordinal generations: `iPod Video (5.5th Generation)` → `iPod Video 5.5G`. The 5.5G iPod Video previously surfaced its full upstream string in short-label cells because the shortener's regex only matched integer ordinals (`3rd`, `5th`). diff --git a/.changeset/sidecar-artwork-and-adapter-fallback.md b/.changeset/sidecar-artwork-and-adapter-fallback.md deleted file mode 100644 index 200eb690..00000000 --- a/.changeset/sidecar-artwork-and-adapter-fallback.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@podkit/core': minor -'podkit': minor ---- - -Sidecar artwork support and executor adapter fallback. - -The sync pipeline now picks up out-of-band artwork that lives alongside the audio file or on the source server, not just embedded pictures: - -- **Directory adapter** detects peer `cover.jpg` / `folder.jpg` / `front.jpg` / `album.jpg` (also `.jpeg` / `.png`, case-insensitive) in the same directory as the audio file. When a file has no embedded picture, the sidecar bytes are used. Under `--check-artwork` the sidecar bytes are hashed and pinned in the sync tag, so swapping a `cover.jpg` for a new image is detected on the next sync. -- **Subsonic adapter** falls back to Navidrome's `getCoverArt` endpoint when the downloaded audio file body has no embedded picture. This closes the gap where a Navidrome library indexed sidecar art on the server but podkit silently dropped it on every sync. -- A one-time placeholder probe runs on every Subsonic `connect()` so Navidrome's static "no cover" image is filtered regardless of `--check-artwork`. - -Embed-in-the-file wins when both are present; the sidecar / API fallback only fires on a miss. Album-level caching means siblings on the same album share a single sidecar read or API request. - -Adapters gain an optional `getArtwork(item): Promise` method on `CollectionAdapter`. The executor calls it through the existing `AlbumArtworkCache` after embedded extraction returns null. - -**Known gap (deferred to TASK-370 / TASK-371 / TASK-372):** mass-storage devices accept the bytes via `setArtworkFromData`, which is a no-op for non-OGG/Opus containers — adapter-fallback bytes reach the device only when the output is OGG/Opus copy (via the existing taglib path) OR the target is an iPod (via the iTunesDB). For other mass-storage outputs the bytes are dropped silently today; the e2e artwork matrix fences those cells with a `[BUG] TASK-370` skip rather than failing the suite. diff --git a/.changeset/sidecar-cleanup-and-broader-orphan-check.md b/.changeset/sidecar-cleanup-and-broader-orphan-check.md deleted file mode 100644 index f313b570..00000000 --- a/.changeset/sidecar-cleanup-and-broader-orphan-check.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@podkit/core': minor -'podkit': minor ---- - -Sidecar lifecycle cleanup + broader orphan-files surface (TASK-375). - -**Sync-time sidecar cleanup (mass-storage, sidecar-primary devices).** -`MassStorageAdapter.removeTrack` and `relocateTrack` now drop the -album's `cover.jpg` when the last managed audio file leaves the -directory. The delete is queued at the moment of removal and flushed -in a new `save()` stage that re-evaluates the predicate per entry, so -a re-add inside the same save cycle (whether through `writeSidecar` or -via a pipeline that skipped artwork on a hash match) cleanly cancels -the queued delete. The previous behaviour left a dangling `cover.jpg` -and a stale manifest entry forever; sync-time cleanup keeps the -invariant "every managed sidecar has at least one managed audio -sibling in its dir" alive. - -**Doctor's orphan-files check no longer filters by extension.** The -mass-storage walker previously surfaced only audio/video files as -orphan candidates. Any other file in your content directories -(sidecar images, lyrics `.lrc`, playlist `.m3u`, stray documents) was -silently dropped. Now the check considers any non-debris file in the -configured content roots — confirmation-gated repair stays unchanged -so you review the list before anything is deleted. This is the -backstop for sidecars on devices that synced before sync-time cleanup -existed, and surfaces unmanaged user-placed files in podkit's -territory you may want to clear out. - -**Migration:** on a rockbox device with a pre-existing user-placed -`cover.jpg` that podkit never wrote, the orphan check will now flag -it. The confirmation prompt is your safety; review before repairing. -After deletion, the next sync re-issues a managed sidecar. diff --git a/.changeset/sync-engine-refactor.md b/.changeset/sync-engine-refactor.md deleted file mode 100644 index c30373d9..00000000 --- a/.changeset/sync-engine-refactor.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@podkit/core": minor -"podkit": minor ---- - -Refactor sync engine to be fully content-type-agnostic with per-handler operation types. - -**Breaking:** `createMusicHandler()` and `createVideoHandler()` now take a config object at construction instead of using `setTransformsConfig()`/`setExecutionConfig()`. Removed `HandlerDiffOptions`, `HandlerPlanOptions`, `MusicExecutionConfig` types. Renamed `MusicExecutor` to `MusicPipeline`. Removed legacy planner functions (`createMusicPlan`, `planVideoSync` and related helpers). - -**New:** `MusicSyncConfig`, `VideoSyncConfig`, `MusicTrackClassifier`, `VideoTrackClassifier`, `MusicOperationFactory`, `MusicOperation`, `VideoOperation`, `BaseOperation` types. Handlers now own their operation types via `TOp` type parameter on `ContentTypeHandler`. diff --git a/.changeset/sync-error-handling-architecture.md b/.changeset/sync-error-handling-architecture.md deleted file mode 100644 index f285c6ac..00000000 --- a/.changeset/sync-error-handling-architecture.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Unify sync-engine error and warning handling (architecture sweep) - -Settles error and warning responsibilities across the sync engine. Hard -failures throw typed errors that carry their own category; soft signals -flow through an injected `WarningSink` and surface alongside hard errors -in `SyncOutput.warnings`. `console.warn` / `console.error` is now banned -in core. - -See `documents/architecture/sync/error-handling.md` for the full -responsibility model. - -## Breaking API changes - -### Types - -- **`SyncWarning` and `ExecutionWarning` types removed.** Replaced by a - single `Warning` type with `phase: 'plan' | 'execute'`. Track - references are now structured (`WarningTrackRef = {artist, title, album?}`) - rather than a mix of `CollectionTrack[]` and an inline object. - - ```ts - // before - import type { SyncWarning, ExecutionWarning } from '@podkit/core'; - // after - import type { Warning, WarningPhase, WarningType, WarningTrackRef } from '@podkit/core'; - ``` - -- **`SyncPlan.warnings`** is now `Warning[]` (always `phase: 'plan'`). -- **`ExecuteResult.warnings`** is now `Warning[]` (always `phase: 'execute'`). -- **`CollectionAdapter.getPlanWarnings?()`** now returns `Warning[]`. - -### New types - -- **`Warning`, `WarningPhase`, `WarningType`, `WarningTrackRef`, - `WarningSink`** — the unified warning surface. -- **`CategorizedSyncError`** — abstract base class for all typed sync - errors. Subclasses declare `readonly category: ErrorCategory` so the - pipeline's categorizer reads it off the class instead of inspecting - the message body. -- **`DatabaseWriteError`** — wraps libgpod failures at the - `IpodAdapter` boundary so iTunesDB errors categorize as `database` - (no retry) rather than falling through to op-type fallback. -- **`PictureWriteError`**, **`MoveError`** — typed siblings of the - existing `TagWriteError` / `SidecarWriteError`, now also extending - `CategorizedSyncError`. - -### `DeviceAdapter` contract - -- New optional **`setWarningSink(sink: WarningSink): void`** method. - Adapters that emit execute-phase warnings (`IpodAdapter`, - `MassStorageAdapter`) must implement it. The pipeline injects its - accumulator sink at execute start. - -## Breaking CLI JSON output changes - -The `sync` command's JSON output replaces the prior two warning fields -with a single unified array: - -```diff -{ - "success": true, -- "planWarnings": [{ "type": "lossy-to-lossy", "message": "...", "trackCount": 2, "tracks": [...] }], -- "executionWarnings": [{ "type": "artwork", "track": "Artist - Title", "message": "..." }] -+ "warnings": [ -+ { -+ "phase": "plan", -+ "type": "lossy-to-lossy", -+ "message": "...", -+ "trackCount": 2, -+ "tracks": [{"artist": "...", "title": "...", "album": "..."}] -+ }, -+ { -+ "phase": "execute", -+ "type": "artwork", -+ "message": "...", -+ "trackCount": 1, -+ "tracks": [{"artist": "...", "title": "...", "album": "..."}] -+ } -+ ] -} -``` - -Filter by `warning.phase` to recover the prior split. Track refs -inside warnings are now structured objects rather than pre-formatted -strings — consumers can format them as they wish. - -## Behaviour fixes - -- **Execute-phase warnings now surface in `--json`.** The - `executionWarnings` field was declared on `SyncOutput` but never - populated by the CLI's real-run path — artwork extraction failures, - iPod portable tag-write misses, and mass-storage vanished-relocate - events were accumulated by the pipeline and silently dropped before - reaching JSON. They now appear in the unified `warnings` array. -- **`IpodAdapter` mutators (`addTrack`, `updateTrack`, `removeTrack`) - wrap libgpod failures in `DatabaseWriteError`.** Without the wrap, - libgpod errors during these mutators would categorize as `copy` via - the op-type fallback and retry once. iTunesDB failures now correctly - categorize as `database` (no retry). -- **Mass-storage picture-write stage normalized to collect-and-aggregate.** - Was `Promise.all` fail-fast with an untyped rejection; now - `runWithConcurrency` + settled-all + `PictureWriteError` + map-cleared- - before-throw, matching the tag-write stage convention. -- **CLI text-mode now prints an execute-phase warning summary** at the - end of a real sync run (grouped by warning type; expand with `-v`). - Previously these warnings were invisible to text-mode users. - -## New text-mode CLI behaviour - -A new `Warnings:` block appears in the sync summary when execute-phase -warnings landed during the run: - -``` -=== Summary === - -Synced 152 items successfully -Duration: 8m 14s - -Warnings: 3 - artwork: 2 - tag-write: 1 - (re-run with -v for details) -``` diff --git a/.changeset/sync-tag-adapter-refactor.md b/.changeset/sync-tag-adapter-refactor.md deleted file mode 100644 index c937d0a8..00000000 --- a/.changeset/sync-tag-adapter-refactor.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@podkit/core': minor ---- - -Internalize raw sync tag functions (`parseSyncTag`, `formatSyncTag`, `writeSyncTag`) from the public API. Sync tag reads now use the typed `DeviceTrack.syncTag` field, and writes use `DeviceAdapter.writeSyncTag()` or the new `update-sync-tag` operation type. Adds `SyncTagUpdate` type and `syncTagsEqual` to the public API. diff --git a/.changeset/sysinfo-extended-auto-identify.md b/.changeset/sysinfo-extended-auto-identify.md deleted file mode 100644 index 6a23fadb..00000000 --- a/.changeset/sysinfo-extended-auto-identify.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor -"@podkit/libgpod-node": minor ---- - -Automated iPod device identification via SysInfoExtended. - -Modern iPods (post-2006) ship without a populated `SysInfo` file after iTunes restore. Without it, libgpod treats the device as generic — artwork breaks, ALAC support is unknown, and database checksums fail on Classic 6/7G and Nano 3G+. podkit now reads SysInfoExtended directly from iPod firmware over USB during `device add`, so first-time setup works with no manual tooling. - -**User-visible:** -- `podkit device add` identifies the exact model (e.g. "iPod nano 8GB Black (3rd Generation)") with no input -- `podkit doctor` detects missing SysInfoExtended and offers `--repair sysinfo-extended` -- `podkit sync` works correctly on first run with full capability detection -- Hash72 (Nano 5G) and HashAB (Nano 6G) devices get clear limitation messages -- SysInfoExtended write is gated on user confirmation during `device add` - -**Core (`@podkit/core`):** -- Unified iPod model registry — single table, both `0x120x`/`0x126x` USB ID ranges, 190+ serial-suffix → model mappings, checksum-type classification per generation -- `ensureSysInfoExtended()` orchestrator: check existing → USB read → validate XML → write -- USB discovery now exposes `serialNumber`, `busNumber`, `deviceAddress`; `resolveUsbDeviceFromPath()` on macOS + Linux -- Readiness pipeline: checksum-aware severity (hash58+ devices fail without SysInfoExtended; pre-checksum devices warn) -- `READINESS_RULES` declarative array replaces ad-hoc `determineLevel()` logic -- New `sysinfo-extended` diagnostic check -- Recognizes `P` / `F` model prefixes in SysInfo - -**libgpod-node (`@podkit/libgpod-node`):** -- `readSysInfoExtendedFromUsb()` N-API binding, resolved via `dlsym` at runtime so it loads gracefully on systems where libgpod lacks the symbol -- Prebuild patches upstream libgpod 0.8.3 to move `itdb_usb.c` from `tools/` into the library; libusb 1.0.27 built from source on all 6 platforms -- `--whole-archive` / `-force_load` linker flags preserve the dlsym symbol in the `.node` binary - -**CLI (`podkit`):** -- `device add` attempts SysInfoExtended read after mount, before DB init; enriches model name in summary -- `doctor` adds suggested-actions section, drops destructive sysinfo guidance -- `device scan` and `doctor` show clearer SysInfo readout diff --git a/.changeset/sysinfo-extended-scsi-fallback.md b/.changeset/sysinfo-extended-scsi-fallback.md deleted file mode 100644 index fee853a2..00000000 --- a/.changeset/sysinfo-extended-scsi-fallback.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"podkit": patch -"@podkit/core": patch -"@podkit/ipod-firmware": patch ---- - -Fix SysInfoExtended SCSI-fallback on macOS for SCSI-only iPods (mini 2G, nano 2G, iPod 5G/5.5G). `device add` and `doctor --repair sysinfo-extended` now correctly fall back from USB → SCSI when the device does not respond to vendor control transfers, instead of failing with a misleading "Could not read device identity from USB" error. - -Internal API changes in `@podkit/ipod-firmware`: - -- **Changed:** `ensureSysInfoExtended(mountPoint, fp, options?)` now takes a full `UsbFingerprint` instead of the previous `{ busNumber, deviceAddress }` shape. Required so the macOS SCSI transport can locate the IOService via vendorId/productId/serialNumber. `UsbDeviceAddress` is removed. -- **Added:** `inquireFirmwareDetailed(fp, opts?)` — like `inquireFirmware` but returns `{ firmware, plan, attempts }` so callers can distinguish which transports were attempted. `inquireFirmware` is unchanged for existing consumers. -- **Added:** `EnsureSysInfoExtendedOptions` type with `readFromUsb`, `resolveModel`, `inquireOptions` fields. Replaces the previous positional `(mountPoint, fp, readFromUsb, resolveModel)` signature. - -Internal API changes in `@podkit/core`: - -- **Added:** `hasCompleteUsbFingerprint(fp): fp is CompleteUsbDevice` type guard exported from `@podkit/core`. -- **Added:** `CompleteUsbDevice` type — a `UsbFingerprint` with vendorId, productId, bus, devnum guaranteed present (serialNumber optional). -- **Changed:** `resolveUsbDeviceFromPath(path)` now also returns `vendorId` and `productId`. Linux extracts from sysfs `idVendor`/`idProduct`; macOS extracts from `system_profiler` JSON. - -User-facing error messages now differentiate between transport failures: "Could not read device identity from USB and SCSI" / "...from USB" / "...from SCSI" / "...no firmware inquiry transport is available on this system" / "...returned data but it could not be parsed". diff --git a/.changeset/sysinfo-modelnum-mismatch-check.md b/.changeset/sysinfo-modelnum-mismatch-check.md deleted file mode 100644 index 4b85c588..00000000 --- a/.changeset/sysinfo-modelnum-mismatch-check.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"podkit": patch -"@podkit/core": patch ---- - -New `podkit doctor` check `sysinfo-modelnum-mismatch` detects when the on-disk classic SysInfo file's `ModelNumStr` disagrees with the firmware-derived identity (e.g. SysInfo manually edited, or files copied from another iPod). Offers `--repair sysinfo-modelnum-mismatch` to overwrite the on-disk file with firmware-derived data. Identified during the TERAPOD (iPod 5G with iFlash mod) inventory pass — the SysInfo claimed `MA147` (5G) while the serial said `V9M`/`A446` (5.5G). diff --git a/.changeset/test-fixtures-mini-tracks-library.md b/.changeset/test-fixtures-mini-tracks-library.md deleted file mode 100644 index 1186e2cb..00000000 --- a/.changeset/test-fixtures-mini-tracks-library.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@podkit/test-fixtures": minor -"@podkit/core": patch ---- - -`@podkit/test-fixtures`: expose synthetic-track generators as a library - -Adds a library entry (`src/lib.ts`) exposing `generateMiniFlac`, `generateMiniMp3`, `generateMiniM4a`, `generateMiniOggVorbis`, and `generateMiniOggOpus`. Each helper writes a single short sine-tone file in the requested codec/container with optional metadata. Integration tests that need real audio for tag round-trip coverage now import these from `@podkit/test-fixtures` rather than re-implementing the ffmpeg invocation inline. - -Each generator calls a new `requireEncoder()` guard before invoking ffmpeg. If the host's ffmpeg is missing the codec's encoder, the helper throws a clear error with platform-aware install hints. There is also an explicit `bun run --filter @podkit/test-fixtures check-ffmpeg` script that verifies the full set of required encoders against the host environment in one shot. - -The mass-storage tag writer integration test (`packages/podkit-core/src/device/mass-storage-tag-writer.integration.test.ts`) drops its inline `generateOgg`, `generateOpus`, `generateFlac`, `generateM4a`, `generateMp3` helpers and the `HAS_LIBVORBIS` skip predicate. The OGG Vorbis tests now run unconditionally — they fail loudly with an install hint when libvorbis is absent rather than skipping silently. - -Developer docs (`docs/developers/development.md`) updated to point macOS contributors at the `homebrew-ffmpeg/ffmpeg` tap for full encoder coverage. diff --git a/.changeset/track-metadata-polish.md b/.changeset/track-metadata-polish.md deleted file mode 100644 index 2573e228..00000000 --- a/.changeset/track-metadata-polish.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor -"@podkit/devices-mass-storage": minor ---- - -Polish on the convergent-metadata work (TASK-327 follow-up): - -- **Tag-write concurrency cap.** `save()` now caps in-flight tag writes at 16 via a small `runWithConcurrency` helper instead of firing every pending write at once. Avoids `EMFILE` on large libraries. -- **Aggregated tag-write errors.** Failure messages now begin with `tag write failed` so the executor's error categorizer classifies them as file-I/O (`copy`) rather than risking a path-keyword mis-classification. -- **WAV/AIFF on mass-storage.** Podkit transcodes WAV and AIFF source files to a managed codec before placing them on a mass-storage device, even when the device firmware can play them. RIFF/IFF tag-writing is unreliable. Presets continue to list these codecs for documentation. iPod is unaffected (libgpod / iTunesDB handle metadata for WAV/AIFF). -- **OGG Vorbis tag round-trip tests.** Now run on builds with libvorbis (skipped automatically when absent). -- **Shared TagFields helpers.** `buildTagFieldsFromInput` and `diffTagFields` replace three duplicate field-by-field walks across adapters. -- **`TransferMode` type unified.** Removed `'fast' | 'optimized' | 'portable'` duplication between `DeviceTrackInput`, `DeviceTrackMetadata`, and the canonical `TransferMode` in `transcode/types.ts`. Drops several inline type casts. -- **Docs.** `transferMode` now has a dedicated section in `docs/reference/config-file.md` explaining the iPod vs mass-storage contract and migration churn. `pathTemplate` (from the prior release) and `PODKIT_PATH_TEMPLATE` are now documented in the config reference and environment-variables reference. diff --git a/.changeset/transfer-mode.md b/.changeset/transfer-mode.md deleted file mode 100644 index 7c764855..00000000 --- a/.changeset/transfer-mode.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Add three-tier transfer mode system controlling how files are prepared for the device. - -**Transfer modes:** -- `fast` (default): optimizes for sync speed — direct-copies compatible files, strips artwork from transcodes -- `optimized`: strips embedded artwork from all file types (including MP3, M4A, ALAC copies) via FFmpeg stream-copy, reducing storage usage without re-encoding -- `portable`: preserves embedded artwork in all files for use outside the iPod ecosystem - -**Configuration:** -- `transferMode` config option (global and per-device) -- `--transfer-mode` CLI flag -- `PODKIT_TRANSFER_MODE` environment variable - -**Selective re-processing:** -- `--force-transfer-mode` flag re-processes only tracks whose transfer mode doesn't match the current setting -- `PODKIT_FORCE_TRANSFER_MODE` environment variable -- Works on all file types including direct copies (unlike `--force-transcode` which only affects transcoded tracks) - -**Device inspection:** -- `podkit device music` and `podkit device video` stats show transfer mode distribution -- Missing transfer field flagged alongside missing artwork hash in sync tag summary -- New `syncTagTransfer` field available in `--tracks --fields` for querying transfer mode data -- Dry-run output shows configured transfer mode - -**Under the hood:** -- Granular operation types: `add-direct-copy`, `add-optimized-copy`, `add-transcode` (and upgrade equivalents) -- Sync tags written to all tracks including direct copies (`quality=copy`) -- `DeviceCapabilities` abstraction for device-aware sync decisions -- Sync tag field `transfer=` tracks which mode was used per track diff --git a/.changeset/tty-aware-interactive-output.md b/.changeset/tty-aware-interactive-output.md deleted file mode 100644 index 7d4bca1e..00000000 --- a/.changeset/tty-aware-interactive-output.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"podkit": patch ---- - -Move spinners and progress bars to stderr and auto-suppress when stdout is not a TTY. Adds `--no-tty` flag for explicit suppression. Piped output (e.g. `podkit collection music --format json | jq .`) now produces clean stdout without needing `--quiet`. diff --git a/.changeset/udev-rule-usb-subsystem.md b/.changeset/udev-rule-usb-subsystem.md deleted file mode 100644 index f1f74634..00000000 --- a/.changeset/udev-rule-usb-subsystem.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"podkit": patch -"@podkit/core": patch ---- - -Extend the podkit udev rule to grant Apple-vendor USB device access (`/dev/bus/usb//`) in addition to the existing SCSI generic (`/dev/sg*`) coverage. Linux libusb-based firmware inquiry now works without sudo from SSH sessions, headless boxes, Docker containers, and CI — the SSH-session permission gap previously closed only the SCSI half. `podkit doctor --repair udev-rule` installs the extended rule and cleans up any legacy filename from previous installs. diff --git a/.changeset/unsupported-device-cascade.md b/.changeset/unsupported-device-cascade.md deleted file mode 100644 index 87741907..00000000 --- a/.changeset/unsupported-device-cascade.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor ---- - -Unify the unsupported-device UX across `podkit device add`, `device scan`, `device info`, `sync`, and `doctor`. Every command now composes identity via the same cascade primitive (`resolveIpodModel(bag)`) — no command re-implements the check, no command leaks `libgpod` into user-facing copy. - -Key behaviour changes: -- `device add` on an unsupported device (hashAB nano, etc.) now asks "Add anyway? [y/N]" rather than hard-refusing. Confirmed devices are recorded with `unsupported: true` in config; `--yes` flips the default to accept. -- `device add` against an iOS device (iPod touch) now surfaces the canonical unsupported message instead of the generic "No iPod devices found". -- `device scan` headers show the resolved model name (e.g. "iPod touch 5th generation") instead of "Unknown iPod (USB only)". -- `sync --dry-run` refuses cleanly on unsupported devices with the canonical message — no track plan generated. -- `sync` on a supported device with SysInfoExtended present resolves identity via the cascade; the legacy "Could not identify iPod model" warning is gone for that case. -- `device info` renders the cascade `displayName` instead of the libgpod-derived `info.device.modelName`. -- `doctor` on an unsupported device suppresses repair suggestions that would mutate device state and surfaces the canonical unsupported message instead. - -Wording is centralised in `@podkit/core` (`makeUnsupportedReasonFromAssessment` / `makeUnsupportedReasonFromModel`) — every consumer imports. diff --git a/.changeset/usb-enumeration-classify-refactor.md b/.changeset/usb-enumeration-classify-refactor.md deleted file mode 100644 index f0d72b2e..00000000 --- a/.changeset/usb-enumeration-classify-refactor.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"podkit": patch -"@podkit/core": patch -"@podkit/devices-ipod": patch -"@podkit/devices-mass-storage": patch ---- - -Fix `podkit device scan` reporting phantom "Unknown iPod (USB only)" entries for non-iPod USB peripherals (mice, hubs, Thunderbolt docks, Ethernet adapters, USB drives). Each phantom suggested `podkit device init` — a destructive operation that could mutate an unrelated device. - -Root cause was architectural: a single `discoverUsbIpods()` function mixed three concerns — USB enumeration, iPod-domain enrichment, and the function name's implied filter (which it didn't actually do). Refactored into clean layers: - -- **`@podkit/core` — pure USB enumeration.** `enumerateUsb()` returns `EnumeratedUsbDevice[]` with vendor/product/serial/bus/devnum/diskIdentifier ONLY. No iPod-domain knowledge. -- **`@podkit/devices-ipod` — iPod classifier.** `classifyAsIpod(dev)` returns `IpodClassification | null` (matches Apple-vendor with iPod or iOS PIDs). -- **`@podkit/devices-mass-storage` — mass-storage classifier.** `classifyAsMassStorage(dev)` returns `MassStorageClassification | null` (matches `USB_PRESET_HINTS` entries like Echo Mini). -- **`@podkit/core` composer.** `classifyUsbDevices()` runs both classifiers and returns recognized devices as a tagged union; drops unknown peripherals. -- **CLI `device scan`** now calls `enumerateUsb()` → `classifyUsbDevices()` → renders by `kind`. No domain logic in the command layer. - -Added `kind: 'mass-storage'` rendering branch so mass-storage DAPs are no longer mis-labeled as "Unknown iPod". - -Removed `discoverUsbIpods` and the leaky `UsbDiscoveredDevice` type (which previously carried iPod-domain fields). Adding a new mass-storage device now means adding one entry to `USB_PRESET_HINTS` — no `@podkit/core` change required. Adding a new iPod generation means updating `@podkit/devices-ipod` tables — no other package changes. - -Also split `usb-discovery.ts` into `usb-enumeration.ts` (bus walk) + `usb-path-resolution.ts` (mount-path → fingerprint resolver) since those two concerns were unrelated. diff --git a/.changeset/usb-error-reporting.md b/.changeset/usb-error-reporting.md deleted file mode 100644 index f0f96f1d..00000000 --- a/.changeset/usb-error-reporting.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"podkit": patch -"@podkit/core": patch -"@podkit/libgpod-node": patch ---- - -Fix `doctor --repair sysinfo-extended` showing unhelpful "Could not read device identity from USB" with no detail. The native USB binding now throws descriptive errors (e.g. "USB control transfer failed (bus 3, device 4)") instead of returning null silently. Also fix all doctor repair intro messages — they incorrectly said "Repairing X for N tracks" even for non-track operations like SysInfoExtended and orphan cleanup. Intro messages now use each repair's own description. diff --git a/.changeset/usb-inquiry-consolidation.md b/.changeset/usb-inquiry-consolidation.md deleted file mode 100644 index ccc1c4b3..00000000 --- a/.changeset/usb-inquiry-consolidation.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@podkit/libgpod-node': minor -'@podkit/ipod-firmware': minor -'podkit': patch ---- - -USB firmware inquiry consolidated into @podkit/ipod-firmware (P2 — m-18 device-capability architecture). - -**Breaking change in `@podkit/libgpod-node`:** The `readSysInfoExtendedFromUsb` function has been removed from the package's public exports. All in-tree callers were already routed through `@podkit/ipod-firmware` since P1 — only external consumers of `@podkit/libgpod-node` who called this function directly are affected. - -`@podkit/ipod-firmware` now owns the complete firmware inquiry surface: SCSI (Linux SG_IO + macOS IOKit) and USB (direct libusb-1.0 via koffi FFI). The P1 transitional shim that delegated USB reads to libgpod-node has been replaced by a native TypeScript implementation. No API change is visible to callers of `@podkit/ipod-firmware`. - -`@podkit/libgpod-node` no longer requires libusb at build or runtime. Distro packagers can now build the native binding without `libusb-1.0-0-dev` (Debian/Ubuntu), `libusb-devel` (Fedora/RHEL), or equivalent system packages. The `itdb_usb.c` patch, the `dlsym` shim, and the libusb pkg-config dependency have all been removed from the binding. - -No user-facing CLI behaviour changes. `podkit doctor` inquiry checks, `podkit device scan`, and all sync paths behave identically to P1. diff --git a/.changeset/usb-via-npm-package.md b/.changeset/usb-via-npm-package.md deleted file mode 100644 index dfdb8882..00000000 --- a/.changeset/usb-via-npm-package.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"podkit": minor -"@podkit/core": minor -"@podkit/ipod-firmware": minor ---- - -Replace koffi-based libusb FFI with the `usb` npm package for USB firmware inquiry, eliminating the runtime libusb system dependency. - -The `@podkit/ipod-firmware` USB transport now uses the `usb` npm package, whose prebuilt N-API bindings statically link libusb. End-user binaries embed that prebuild via Bun `--compile`; no system `libusb-1.0` is required at runtime. - -Public-surface changes in `@podkit/ipod-firmware`: - -- **Removed:** `loadLibusb`, `LibusbBinding`, `LibusbPtr`, `LibusbLoadResult`, `_resetLibusbCacheForTests`. The koffi-shaped binding interface is gone. -- **Added:** `loadUsb`, `UsbBinding`, `UsbDeviceHandle`, `UsbLoadResult`, `_resetUsbCacheForTests`. Higher-level `withOpenDevice(bus, devnum, fn)` seam — implementations handle enumeration, open, and cleanup internally. -- **Added:** `setLogger(fn | null)`, `FirmwareLogger`, `FirmwareLogEvent`. Library no longer writes to stderr/stdout; consumers install a receiver and decide format/destination. The CLI installs one when `-v` is passed. -- **Added:** `@podkit/ipod-firmware/bundle` subpath export with `bundleUsbNative(nativeModule)` for single-file binary builds. See `agents/ipod-firmware.md` for the staging recipe. -- **Renamed:** `UsbInquiryError.libusbCode` → `UsbInquiryError.libusbStatus`. The new field carries `LIBUSB_TRANSFER_*` status codes (positive enum) from the `usb` npm package, not the negative `LIBUSB_ERROR_*` codes the koffi path returned. - -Doctor's `inquiry-methods` check no longer reports libusb availability — the USB transport is bundled and always present in shipped binaries. The check now reports SCSI transport availability only, which remains user-actionable on Linux (udev permissions) and macOS (iPodDriver.kext). diff --git a/.changeset/user-defined-mass-storage-presets.md b/.changeset/user-defined-mass-storage-presets.md deleted file mode 100644 index 08141b0c..00000000 --- a/.changeset/user-defined-mass-storage-presets.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"podkit": minor ---- - -Add user-defined mass-storage device presets via the new `[presets.]` config section. - -Declare a custom DAP in `~/.config/podkit/config.toml`: - -```toml -[presets.my-walkman] -extends = "generic" -manufacturer = "Sony" -productName = "NW-A105" -supportedAudioCodecs = ["aac", "flac", "mp3"] -artworkMaxResolution = 240 -musicDir = "MUSIC" -``` - -Then add a device that uses it: - -```sh -podkit device add -d walkman --type my-walkman --path /Volumes/MyWalkman -``` - -`--type` previously rejected any value that wasn't `ipod`, `echo-mini`, `rockbox`, or `generic`. It now also accepts any preset id declared in the config. Built-in ids remain authoritative; `[presets.echo-mini]` collisions are refused at load time. - -`device list`, `device info`, sync, and doctor mass-storage paths consult the merged registry so user-preset content-path and capability defaults flow through correctly. Two devices typed to the same user preset id resolve independently — they share the preset baseline but per-device overrides apply on top. - -When a user preset declares `wav` or `aiff` in `supportedAudioCodecs`, the loader emits the same warning that `[devices.X]` overrides already produce: podkit transcodes sources in those formats rather than direct-copying. diff --git a/bun.lock b/bun.lock index a02d1623..8684e332 100644 --- a/bun.lock +++ b/bun.lock @@ -18,7 +18,7 @@ }, "packages/compatibility": { "name": "@podkit/compatibility", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "@podkit/libgpod-node": "workspace:*", }, @@ -36,7 +36,7 @@ }, "packages/device-types": { "name": "@podkit/device-types", - "version": "0.0.1", + "version": "0.1.0", "devDependencies": { "@types/bun": "latest", "bun-types": "latest", @@ -44,7 +44,7 @@ }, "packages/devices-ipod": { "name": "@podkit/devices-ipod", - "version": "0.0.1", + "version": "0.1.0", "dependencies": { "@podkit/device-types": "workspace:*", "@podkit/ipod-firmware": "workspace:*", @@ -56,7 +56,7 @@ }, "packages/devices-mass-storage": { "name": "@podkit/devices-mass-storage", - "version": "0.0.1", + "version": "0.1.0", "dependencies": { "@podkit/device-types": "workspace:*", }, @@ -67,7 +67,7 @@ }, "packages/docs-site": { "name": "@podkit/docs-site", - "version": "0.0.2", + "version": "0.0.3", "dependencies": { "@astrojs/starlight": "^0.33.0", "@podkit/compatibility": "workspace:*", @@ -94,7 +94,7 @@ }, "packages/ipod-firmware": { "name": "@podkit/ipod-firmware", - "version": "0.0.1", + "version": "0.1.0", "dependencies": { "@podkit/device-types": "workspace:*", "koffi": "^2.13.0", @@ -134,7 +134,7 @@ }, "packages/libgpod-node": { "name": "@podkit/libgpod-node", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "node-addon-api": "^8.3.1", }, @@ -149,7 +149,7 @@ }, "packages/podkit-cli": { "name": "podkit", - "version": "0.6.0", + "version": "0.7.0", "bin": { "podkit": "./dist/main.js", }, @@ -175,7 +175,7 @@ }, "packages/podkit-core": { "name": "@podkit/core", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@ctrl/video-filename-parser": "^5.4.1", "@podkit/device-types": "workspace:*", @@ -197,7 +197,7 @@ }, "packages/podkit-daemon": { "name": "@podkit/daemon", - "version": "0.2.2", + "version": "0.3.0", "dependencies": { "@podkit/core": "workspace:*", }, @@ -208,7 +208,7 @@ }, "packages/podkit-docker": { "name": "@podkit/docker", - "version": "0.2.3", + "version": "0.2.4", "dependencies": { "@podkit/daemon": "workspace:*", "podkit": "workspace:*", @@ -244,7 +244,7 @@ }, "test-packages/device-testing": { "name": "@podkit/device-testing", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "@podkit/core": "workspace:*", "@podkit/device-types": "workspace:*", @@ -258,7 +258,7 @@ }, "test-packages/device-testing-daemon": { "name": "@podkit/device-testing-daemon", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "@podkit/device-testing": "workspace:*", }, @@ -270,7 +270,7 @@ }, "test-packages/e2e-shared": { "name": "@podkit/e2e-shared", - "version": "0.0.0", + "version": "0.0.1", "dependencies": { "@podkit/gpod-testing": "workspace:*", "@podkit/test-fixtures": "workspace:*", @@ -303,7 +303,7 @@ }, "test-packages/e2e-vm-tests": { "name": "@podkit/e2e-vm-tests", - "version": "0.0.0", + "version": "0.0.1", "dependencies": { "@podkit/core": "workspace:*", "@podkit/device-testing": "workspace:*", @@ -330,7 +330,7 @@ }, "test-packages/test-fixtures": { "name": "@podkit/test-fixtures", - "version": "0.0.0", + "version": "0.1.0", "devDependencies": { "@types/bun": "latest", "bun-types": "latest", diff --git a/packages/compatibility/CHANGELOG.md b/packages/compatibility/CHANGELOG.md index e229b682..30a85186 100644 --- a/packages/compatibility/CHANGELOG.md +++ b/packages/compatibility/CHANGELOG.md @@ -1,5 +1,12 @@ # @podkit/compatibility +## 0.0.2 + +### Patch Changes + +- Updated dependencies [[`22dddf4`](https://github.com/jvgomg/podkit/commit/22dddf4803f4cfd7b004d80dffd83878a68b10f2), [`1ec30ac`](https://github.com/jvgomg/podkit/commit/1ec30acca1109178012db3913a60967a2087fb5b), [`6000868`](https://github.com/jvgomg/podkit/commit/6000868830d9437a6fff3c1a77adb254d9579fe7), [`4598f8f`](https://github.com/jvgomg/podkit/commit/4598f8f3347cf40b94fdf1585215e5b0f54d9cf6)]: + - @podkit/libgpod-node@0.2.0 + ## 0.0.1 ### Patch Changes diff --git a/packages/compatibility/package.json b/packages/compatibility/package.json index b84e1f55..c61984c3 100644 --- a/packages/compatibility/package.json +++ b/packages/compatibility/package.json @@ -1,7 +1,7 @@ { "name": "@podkit/compatibility", "private": true, - "version": "0.0.1", + "version": "0.0.2", "description": "iPod model compatibility data — single source of truth for E2E tests and docs", "type": "module", "main": "./src/index.ts", diff --git a/packages/device-types/CHANGELOG.md b/packages/device-types/CHANGELOG.md new file mode 100644 index 00000000..220b3c26 --- /dev/null +++ b/packages/device-types/CHANGELOG.md @@ -0,0 +1,50 @@ +# @podkit/device-types + +## 0.1.0 + +### Minor Changes + +- [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af) Thanks [@jvgomg](https://github.com/jvgomg)! - Disambiguate codec from container: `AudioCodec` value `'ogg'` is renamed to `'vorbis'` + + The `AudioCodec` slot previously used `'ogg'` to mean "OGG Vorbis." That conflated the OGG container with the Vorbis stream codec and could not represent Vorbis-in-OGG vs Opus-in-OGG as distinct device capabilities — Echo Mini (which plays Vorbis but hides `.opus` files) could not be modelled accurately. The codec slot now names the audio stream codec; `'vorbis'` replaces `'ogg'` in device presets and config. + + Configs containing `supportedAudioCodecs = ["…", "ogg", "…"]` under `[devices.*]` are migrated automatically by `podkit migrate` (config version 1 → 2). The migration is purely a string substitution inside the `supportedAudioCodecs` array; comments and surrounding formatting are preserved. + + Also lands as type-level groundwork for the future container-aware sync work: `AudioContainer`, `AUDIO_CONTAINERS`, `CODEC_CANONICAL_CONTAINER`, and an optional `DeviceCapabilities.containerConstraints` field. These are declared and exported but not yet read by the planner; they are placeholders for the upcoming Phase 2 work documented in the container-aware sync PRD. + + `DirectoryAdapter` now uses each `.ogg` file's probed stream codec (already populated by `music-metadata`) to distinguish Vorbis, Opus, and OGG-FLAC — same pattern as the existing AAC/ALAC distinction for `.m4a`. `SubsonicAdapter` additionally checks the API's `contentType` field for Opus-in-`.ogg`. The Subsonic check is best-effort because most Subsonic servers report container MIME (`audio/ogg`) regardless of stream codec; deeper probing is deferred until evidence of real-world impact. + + User-facing reference page added at `docs/reference/codec-support.md` explaining the codec/container model and what each `AudioCodec` value means. + +- [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b) Thanks [@jvgomg](https://github.com/jvgomg)! - Consolidate the two ways podkit expressed "this device is unsupported" into one canonical shape. `ReadinessUnsupportedReason` moves to `@podkit/device-types` (its natural home), and `resolveIpodModel(bag)` now returns it directly on `IpodModel.unsupportedReason` instead of the bare-string `notSupportedReason`. The bridge functions in `@podkit/core` (`makeUnsupportedReasonFromModel`, `makeUnsupportedReasonFromAssessment`) are removed — consumers read `model.unsupportedReason` directly. Internal refactor; user-facing CLI behaviour is unchanged. + +- [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6) Thanks [@jvgomg](https://github.com/jvgomg)! - P4 — device-capability architecture complete. All in-tree migration finished; deprecated shims removed. + + **Breaking changes in `@podkit/core`:** The following symbols have been removed from the public API: `createIpodCapabilities`, `LibgpodDeviceInfo`, `DEVICE_PRESETS`, `DevicePreset`, `getDevicePreset`, and `resolveDeviceCapabilities`. Callers must migrate before upgrading. + + Migration guide: + - `createIpodCapabilities(libgpodInfo)` → `resolveCapabilities(identity)` (preferred, identity-driven) or `resolveIpodModelCapabilities(modelFromLibgpodInfo(libgpodInfo))` for callers that genuinely hold libgpod data. + - `getDevicePreset(deviceType)` → `BUILT_IN_PRESETS[deviceType]` from `@podkit/devices-mass-storage`. + - `DEVICE_PRESETS` → `BUILT_IN_PRESETS` from `@podkit/devices-mass-storage`. + - `resolveDeviceCapabilities(type, overrides)` → `resolveCapabilities(identity, { overrides })` from `@podkit/core`. + - The `core/device/sysinfo-extended` shim path is gone; import `readSysInfoExtended`, `writeSysInfoExtended`, and `ensureSysInfoExtended` from `@podkit/ipod-firmware` directly. + + See ADR-295.07 for the full architectural rationale. + + **New in `@podkit/ipod-firmware`:** SysInfoExtended file I/O is now owned by this package: `readSysInfoExtended`, `writeSysInfoExtended`, `ensureSysInfoExtended`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR`. Diagnostic helpers `compareSysInfoConsistency` and `normaliseFireWireGuid` are also exported. `ParsedFirmware` gains the optional `modelNumber` field (populated from the `ModelNumStr` plist key when present). + + **New in `@podkit/device-types`:** `IpodModel`, `IpodChecksumType`, `IpodGenerationId`, `IpodGenerationIdLike`, `IpodModelSource`, and `IPOD_GENERATION_IDS` are now exported from this package (canonical home). `UsbConnectionInfo` has been removed — use `UsbFingerprint` instead. `IpodIdentity.notSupportedReason` is added for devices identified as iPods that podkit cannot fully support (e.g. iOS-mode devices). `DeviceCapabilities.artworkMaxResolution` is now `number | null` (null when the generation has no known limit or artwork is unsupported). + + **In `@podkit/devices-ipod`:** `IpodGeneration.supported` and `IpodGeneration.artworkMaxResolution: number | null` are new fields on every generation entry. `lookupByFamilyId` and `FAMILY_ID_TO_GENERATION` are now exported. `unsupported.ts` is populated with comprehensive Apple iOS device PIDs to allow early rejection of phones and tablets at the USB identification stage. + + No user-facing CLI behaviour changes. `podkit device scan`, `podkit device info`, and all sync paths behave identically to P3. + +- [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4) Thanks [@jvgomg](https://github.com/jvgomg)! - Add SCSI firmware inquiry for iPod identification (P1 — m-18 device-capability architecture). + + `@podkit/device-types` (first published release) provides the canonical shared type definitions — `DeviceCapabilities`, `DeviceIdentity`, `ParsedFirmware`, and `DeviceProvider` — used across the podkit monorepo without circular dependencies. + + `@podkit/ipod-firmware` (first published release) implements iPod firmware inquiry via SCSI (Linux SG_IO + macOS IOKit, using koffi FFI) with USB fallback through the existing libgpod-node binding. Devices that previously failed identification over USB — including iPod mini 2G, nano 2G, and some iPod 5G Video configurations — can now be identified via SCSI. The orchestrator probes available transports at startup, prefers USB when both are available, and falls back to SCSI transparently. + + `@podkit/core` now routes `ensureSysInfoExtended` through the new orchestrator with SCSI fallback, and registers two new `podkit doctor` checks: `inquiry-methods` (reports which transports are available on this host) and `sysinfo-consistency` (validates that the on-disk SysInfo file matches the live firmware read). EACCES errors from SCSI include step-by-step recovery instructions. + + `podkit` CLI gains `--repair udev-rule` in `podkit doctor` to install the Linux udev rule that grants non-root `/dev/sg*` access, and surfaces the new doctor checks in the readiness output. diff --git a/packages/device-types/package.json b/packages/device-types/package.json index 1439f28f..47fac360 100644 --- a/packages/device-types/package.json +++ b/packages/device-types/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/device-types", - "version": "0.0.1", + "version": "0.1.0", "description": "Shared device capability and identity types for podkit", "type": "module", "main": "./dist/index.js", diff --git a/packages/devices-ipod/CHANGELOG.md b/packages/devices-ipod/CHANGELOG.md new file mode 100644 index 00000000..3c021b28 --- /dev/null +++ b/packages/devices-ipod/CHANGELOG.md @@ -0,0 +1,82 @@ +# @podkit/devices-ipod + +## 0.1.0 + +### Minor Changes + +- [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af) Thanks [@jvgomg](https://github.com/jvgomg)! - Disambiguate codec from container: `AudioCodec` value `'ogg'` is renamed to `'vorbis'` + + The `AudioCodec` slot previously used `'ogg'` to mean "OGG Vorbis." That conflated the OGG container with the Vorbis stream codec and could not represent Vorbis-in-OGG vs Opus-in-OGG as distinct device capabilities — Echo Mini (which plays Vorbis but hides `.opus` files) could not be modelled accurately. The codec slot now names the audio stream codec; `'vorbis'` replaces `'ogg'` in device presets and config. + + Configs containing `supportedAudioCodecs = ["…", "ogg", "…"]` under `[devices.*]` are migrated automatically by `podkit migrate` (config version 1 → 2). The migration is purely a string substitution inside the `supportedAudioCodecs` array; comments and surrounding formatting are preserved. + + Also lands as type-level groundwork for the future container-aware sync work: `AudioContainer`, `AUDIO_CONTAINERS`, `CODEC_CANONICAL_CONTAINER`, and an optional `DeviceCapabilities.containerConstraints` field. These are declared and exported but not yet read by the planner; they are placeholders for the upcoming Phase 2 work documented in the container-aware sync PRD. + + `DirectoryAdapter` now uses each `.ogg` file's probed stream codec (already populated by `music-metadata`) to distinguish Vorbis, Opus, and OGG-FLAC — same pattern as the existing AAC/ALAC distinction for `.m4a`. `SubsonicAdapter` additionally checks the API's `contentType` field for Opus-in-`.ogg`. The Subsonic check is best-effort because most Subsonic servers report container MIME (`audio/ogg`) regardless of stream codec; deeper probing is deferred until evidence of real-world impact. + + User-facing reference page added at `docs/reference/codec-support.md` explaining the codec/container model and what each `AudioCodec` value means. + +- [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b) Thanks [@jvgomg](https://github.com/jvgomg)! - Consolidate the two ways podkit expressed "this device is unsupported" into one canonical shape. `ReadinessUnsupportedReason` moves to `@podkit/device-types` (its natural home), and `resolveIpodModel(bag)` now returns it directly on `IpodModel.unsupportedReason` instead of the bare-string `notSupportedReason`. The bridge functions in `@podkit/core` (`makeUnsupportedReasonFromModel`, `makeUnsupportedReasonFromAssessment`) are removed — consumers read `model.unsupportedReason` directly. Internal refactor; user-facing CLI behaviour is unchanged. + +### Patch Changes + +- [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6) Thanks [@jvgomg](https://github.com/jvgomg)! - P4 — device-capability architecture complete. All in-tree migration finished; deprecated shims removed. + + **Breaking changes in `@podkit/core`:** The following symbols have been removed from the public API: `createIpodCapabilities`, `LibgpodDeviceInfo`, `DEVICE_PRESETS`, `DevicePreset`, `getDevicePreset`, and `resolveDeviceCapabilities`. Callers must migrate before upgrading. + + Migration guide: + - `createIpodCapabilities(libgpodInfo)` → `resolveCapabilities(identity)` (preferred, identity-driven) or `resolveIpodModelCapabilities(modelFromLibgpodInfo(libgpodInfo))` for callers that genuinely hold libgpod data. + - `getDevicePreset(deviceType)` → `BUILT_IN_PRESETS[deviceType]` from `@podkit/devices-mass-storage`. + - `DEVICE_PRESETS` → `BUILT_IN_PRESETS` from `@podkit/devices-mass-storage`. + - `resolveDeviceCapabilities(type, overrides)` → `resolveCapabilities(identity, { overrides })` from `@podkit/core`. + - The `core/device/sysinfo-extended` shim path is gone; import `readSysInfoExtended`, `writeSysInfoExtended`, and `ensureSysInfoExtended` from `@podkit/ipod-firmware` directly. + + See ADR-295.07 for the full architectural rationale. + + **New in `@podkit/ipod-firmware`:** SysInfoExtended file I/O is now owned by this package: `readSysInfoExtended`, `writeSysInfoExtended`, `ensureSysInfoExtended`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR`. Diagnostic helpers `compareSysInfoConsistency` and `normaliseFireWireGuid` are also exported. `ParsedFirmware` gains the optional `modelNumber` field (populated from the `ModelNumStr` plist key when present). + + **New in `@podkit/device-types`:** `IpodModel`, `IpodChecksumType`, `IpodGenerationId`, `IpodGenerationIdLike`, `IpodModelSource`, and `IPOD_GENERATION_IDS` are now exported from this package (canonical home). `UsbConnectionInfo` has been removed — use `UsbFingerprint` instead. `IpodIdentity.notSupportedReason` is added for devices identified as iPods that podkit cannot fully support (e.g. iOS-mode devices). `DeviceCapabilities.artworkMaxResolution` is now `number | null` (null when the generation has no known limit or artwork is unsupported). + + **In `@podkit/devices-ipod`:** `IpodGeneration.supported` and `IpodGeneration.artworkMaxResolution: number | null` are new fields on every generation entry. `lookupByFamilyId` and `FAMILY_ID_TO_GENERATION` are now exported. `unsupported.ts` is populated with comprehensive Apple iOS device PIDs to allow early rejection of phones and tablets at the USB identification stage. + + No user-facing CLI behaviour changes. `podkit device scan`, `podkit device info`, and all sync paths behave identically to P3. + +- [`01ecedd`](https://github.com/jvgomg/podkit/commit/01ecedde623ff99e94c5cbda75ff9f9c9ecef632) Thanks [@jvgomg](https://github.com/jvgomg)! - New packages: `@podkit/devices-ipod` (canonical home for iPod generation tables, model lookups, and capability synthesis) and `@podkit/devices-mass-storage` (user-extensible DAP preset framework for Echo Mini, Rockbox, generic, and custom devices). + + Echo Mini is now auto-detected at `device add` — when the USB descriptor matches the known VID/PID (`0x071b`/`0x3203`), no `--type echo-mini` flag is required. + + `enumerateConnectedDevices` is now the recommended way to discover and classify USB devices. It accepts a `providers: DeviceProvider[]` array and returns `EnumeratedDevice[]` carrying both the USB connection info and the provider-produced identity. + + `getCapabilities` in `@podkit/devices-ipod` is libgpod-free. Capability synthesis is purely table-and-firmware-driven; the legacy `createIpodCapabilities` adapter that depended on a live libgpod `LibgpodDeviceInfo` struct is deprecated in `@podkit/core`. Parity is verified across all 29 generations (the 4 that were libgpod `unknown` degenerate cases are now correctly populated from the table). + + Internal re-export shims in `@podkit/core` keep all existing call paths compiling for one release. The shims delegate to `@podkit/devices-ipod` and `@podkit/devices-mass-storage` and will be removed in P4. + +- [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix iPod model identification regressing to "Unknown iPod" after `doctor --repair sysinfo-extended` on pre-2006 devices (mini 2G), and tighten the package-boundary contract so consumers compose identity instead of injecting resolution policy. + + The bug: each consumer of `ensureSysInfoExtended` / `readSysInfoExtended` passed a serial-only `resolveModel` callback. When the 3-character serial suffix wasn't in `tables/serials.ts`, the resolver returned undefined and the device was displayed as "Unknown iPod" — even when a SysInfo file with a known `ModelNumStr` was sitting next to the SysInfoExtended on disk. + + The fix: + - **Removed** `ModelResolver` type and the `resolveModel` callback from `@podkit/ipod-firmware`. `readSysInfoExtended` and `ensureSysInfoExtended` now return a flat `SysInfoIdentity` bag (`firewireGuid?, serialNumber?, modelNumStr?, familyId?`). When a SysInfo file is on disk alongside SysInfoExtended, its `ModelNumStr` is read opportunistically. + - **Callers compose** with `resolveIpodModel(bag)` from `@podkit/devices-ipod`, which cascades modelNumStr → serial → productId → familyId → libgpodGeneration. The CLI no longer makes resolution decisions. + - **Added** `SYSINFO_PATH`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR` exported from `@podkit/ipod-firmware` and re-exported from `@podkit/core`. Consumers use these constants instead of duplicating the literal `iPod_Control/Device/...` paths. + - **Added** `S4G: '9804'` entry to `tables/serials.ts` (mini 2G 4GB Pink, sourced from real hardware, serial `JQ5141TFS4G`). + - **Post-write enrichment.** After `ensureSysInfoExtended` writes the file via USB inquiry, it now re-reads via `readSysInfoExtended` so the post-write identity bag includes `modelNumStr` from the SysInfo neighbour. Eliminates the cosmetic regression where the repair-success message showed a less-specific name than the subsequent `doctor` run. + +- [`f61a83b`](https://github.com/jvgomg/podkit/commit/f61a83b3a2d13612730f174759fd3b86edd42e82) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix `podkit device scan` reporting phantom "Unknown iPod (USB only)" entries for non-iPod USB peripherals (mice, hubs, Thunderbolt docks, Ethernet adapters, USB drives). Each phantom suggested `podkit device init` — a destructive operation that could mutate an unrelated device. + + Root cause was architectural: a single `discoverUsbIpods()` function mixed three concerns — USB enumeration, iPod-domain enrichment, and the function name's implied filter (which it didn't actually do). Refactored into clean layers: + - **`@podkit/core` — pure USB enumeration.** `enumerateUsb()` returns `EnumeratedUsbDevice[]` with vendor/product/serial/bus/devnum/diskIdentifier ONLY. No iPod-domain knowledge. + - **`@podkit/devices-ipod` — iPod classifier.** `classifyAsIpod(dev)` returns `IpodClassification | null` (matches Apple-vendor with iPod or iOS PIDs). + - **`@podkit/devices-mass-storage` — mass-storage classifier.** `classifyAsMassStorage(dev)` returns `MassStorageClassification | null` (matches `USB_PRESET_HINTS` entries like Echo Mini). + - **`@podkit/core` composer.** `classifyUsbDevices()` runs both classifiers and returns recognized devices as a tagged union; drops unknown peripherals. + - **CLI `device scan`** now calls `enumerateUsb()` → `classifyUsbDevices()` → renders by `kind`. No domain logic in the command layer. + + Added `kind: 'mass-storage'` rendering branch so mass-storage DAPs are no longer mis-labeled as "Unknown iPod". + + Removed `discoverUsbIpods` and the leaky `UsbDiscoveredDevice` type (which previously carried iPod-domain fields). Adding a new mass-storage device now means adding one entry to `USB_PRESET_HINTS` — no `@podkit/core` change required. Adding a new iPod generation means updating `@podkit/devices-ipod` tables — no other package changes. + + Also split `usb-discovery.ts` into `usb-enumeration.ts` (bus walk) + `usb-path-resolution.ts` (mount-path → fingerprint resolver) since those two concerns were unrelated. + +- Updated dependencies [[`bb2e637`](https://github.com/jvgomg/podkit/commit/bb2e6374151605d11baf052c452f10a842e5353e), [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af), [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b), [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6), [`a78e5fe`](https://github.com/jvgomg/podkit/commit/a78e5fee4e47293c1935395bb157cb6574782625), [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce), [`eed4126`](https://github.com/jvgomg/podkit/commit/eed4126fe91ff64f00d74e8a2aaaae38ca6d786b), [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4), [`80fe65a`](https://github.com/jvgomg/podkit/commit/80fe65a022c65da512f571a8abf83f9385a649e6), [`4598f8f`](https://github.com/jvgomg/podkit/commit/4598f8f3347cf40b94fdf1585215e5b0f54d9cf6), [`e825ee1`](https://github.com/jvgomg/podkit/commit/e825ee1dd4933ecbfd070dda27f96f43056f0baf)]: + - @podkit/ipod-firmware@0.1.0 + - @podkit/device-types@0.1.0 diff --git a/packages/devices-ipod/package.json b/packages/devices-ipod/package.json index e97f8cf8..dea2e0a8 100644 --- a/packages/devices-ipod/package.json +++ b/packages/devices-ipod/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/devices-ipod", - "version": "0.0.1", + "version": "0.1.0", "description": "iPod generation tables, USB ID lookup, and model identification for podkit", "type": "module", "main": "./dist/index.js", diff --git a/packages/devices-mass-storage/CHANGELOG.md b/packages/devices-mass-storage/CHANGELOG.md new file mode 100644 index 00000000..77a3d9f0 --- /dev/null +++ b/packages/devices-mass-storage/CHANGELOG.md @@ -0,0 +1,77 @@ +# @podkit/devices-mass-storage + +## 0.1.0 + +### Minor Changes + +- [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af) Thanks [@jvgomg](https://github.com/jvgomg)! - Disambiguate codec from container: `AudioCodec` value `'ogg'` is renamed to `'vorbis'` + + The `AudioCodec` slot previously used `'ogg'` to mean "OGG Vorbis." That conflated the OGG container with the Vorbis stream codec and could not represent Vorbis-in-OGG vs Opus-in-OGG as distinct device capabilities — Echo Mini (which plays Vorbis but hides `.opus` files) could not be modelled accurately. The codec slot now names the audio stream codec; `'vorbis'` replaces `'ogg'` in device presets and config. + + Configs containing `supportedAudioCodecs = ["…", "ogg", "…"]` under `[devices.*]` are migrated automatically by `podkit migrate` (config version 1 → 2). The migration is purely a string substitution inside the `supportedAudioCodecs` array; comments and surrounding formatting are preserved. + + Also lands as type-level groundwork for the future container-aware sync work: `AudioContainer`, `AUDIO_CONTAINERS`, `CODEC_CANONICAL_CONTAINER`, and an optional `DeviceCapabilities.containerConstraints` field. These are declared and exported but not yet read by the planner; they are placeholders for the upcoming Phase 2 work documented in the container-aware sync PRD. + + `DirectoryAdapter` now uses each `.ogg` file's probed stream codec (already populated by `music-metadata`) to distinguish Vorbis, Opus, and OGG-FLAC — same pattern as the existing AAC/ALAC distinction for `.m4a`. `SubsonicAdapter` additionally checks the API's `contentType` field for Opus-in-`.ogg`. The Subsonic check is best-effort because most Subsonic servers report container MIME (`audio/ogg`) regardless of stream codec; deeper probing is deferred until evidence of real-world impact. + + User-facing reference page added at `docs/reference/codec-support.md` explaining the codec/container model and what each `AudioCodec` value means. + +- [`c5cba69`](https://github.com/jvgomg/podkit/commit/c5cba6998283663b42659f02b17b194ab256c137) Thanks [@jvgomg](https://github.com/jvgomg)! - Polish on the convergent-metadata work (TASK-327 follow-up): + - **Tag-write concurrency cap.** `save()` now caps in-flight tag writes at 16 via a small `runWithConcurrency` helper instead of firing every pending write at once. Avoids `EMFILE` on large libraries. + - **Aggregated tag-write errors.** Failure messages now begin with `tag write failed` so the executor's error categorizer classifies them as file-I/O (`copy`) rather than risking a path-keyword mis-classification. + - **WAV/AIFF on mass-storage.** Podkit transcodes WAV and AIFF source files to a managed codec before placing them on a mass-storage device, even when the device firmware can play them. RIFF/IFF tag-writing is unreliable. Presets continue to list these codecs for documentation. iPod is unaffected (libgpod / iTunesDB handle metadata for WAV/AIFF). + - **OGG Vorbis tag round-trip tests.** Now run on builds with libvorbis (skipped automatically when absent). + - **Shared TagFields helpers.** `buildTagFieldsFromInput` and `diffTagFields` replace three duplicate field-by-field walks across adapters. + - **`TransferMode` type unified.** Removed `'fast' | 'optimized' | 'portable'` duplication between `DeviceTrackInput`, `DeviceTrackMetadata`, and the canonical `TransferMode` in `transcode/types.ts`. Drops several inline type casts. + - **Docs.** `transferMode` now has a dedicated section in `docs/reference/config-file.md` explaining the iPod vs mass-storage contract and migration churn. `pathTemplate` (from the prior release) and `PODKIT_PATH_TEMPLATE` are now documented in the config reference and environment-variables reference. + +### Patch Changes + +- [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6) Thanks [@jvgomg](https://github.com/jvgomg)! - P4 — device-capability architecture complete. All in-tree migration finished; deprecated shims removed. + + **Breaking changes in `@podkit/core`:** The following symbols have been removed from the public API: `createIpodCapabilities`, `LibgpodDeviceInfo`, `DEVICE_PRESETS`, `DevicePreset`, `getDevicePreset`, and `resolveDeviceCapabilities`. Callers must migrate before upgrading. + + Migration guide: + - `createIpodCapabilities(libgpodInfo)` → `resolveCapabilities(identity)` (preferred, identity-driven) or `resolveIpodModelCapabilities(modelFromLibgpodInfo(libgpodInfo))` for callers that genuinely hold libgpod data. + - `getDevicePreset(deviceType)` → `BUILT_IN_PRESETS[deviceType]` from `@podkit/devices-mass-storage`. + - `DEVICE_PRESETS` → `BUILT_IN_PRESETS` from `@podkit/devices-mass-storage`. + - `resolveDeviceCapabilities(type, overrides)` → `resolveCapabilities(identity, { overrides })` from `@podkit/core`. + - The `core/device/sysinfo-extended` shim path is gone; import `readSysInfoExtended`, `writeSysInfoExtended`, and `ensureSysInfoExtended` from `@podkit/ipod-firmware` directly. + + See ADR-295.07 for the full architectural rationale. + + **New in `@podkit/ipod-firmware`:** SysInfoExtended file I/O is now owned by this package: `readSysInfoExtended`, `writeSysInfoExtended`, `ensureSysInfoExtended`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR`. Diagnostic helpers `compareSysInfoConsistency` and `normaliseFireWireGuid` are also exported. `ParsedFirmware` gains the optional `modelNumber` field (populated from the `ModelNumStr` plist key when present). + + **New in `@podkit/device-types`:** `IpodModel`, `IpodChecksumType`, `IpodGenerationId`, `IpodGenerationIdLike`, `IpodModelSource`, and `IPOD_GENERATION_IDS` are now exported from this package (canonical home). `UsbConnectionInfo` has been removed — use `UsbFingerprint` instead. `IpodIdentity.notSupportedReason` is added for devices identified as iPods that podkit cannot fully support (e.g. iOS-mode devices). `DeviceCapabilities.artworkMaxResolution` is now `number | null` (null when the generation has no known limit or artwork is unsupported). + + **In `@podkit/devices-ipod`:** `IpodGeneration.supported` and `IpodGeneration.artworkMaxResolution: number | null` are new fields on every generation entry. `lookupByFamilyId` and `FAMILY_ID_TO_GENERATION` are now exported. `unsupported.ts` is populated with comprehensive Apple iOS device PIDs to allow early rejection of phones and tablets at the USB identification stage. + + No user-facing CLI behaviour changes. `podkit device scan`, `podkit device info`, and all sync paths behave identically to P3. + +- [`01ecedd`](https://github.com/jvgomg/podkit/commit/01ecedde623ff99e94c5cbda75ff9f9c9ecef632) Thanks [@jvgomg](https://github.com/jvgomg)! - New packages: `@podkit/devices-ipod` (canonical home for iPod generation tables, model lookups, and capability synthesis) and `@podkit/devices-mass-storage` (user-extensible DAP preset framework for Echo Mini, Rockbox, generic, and custom devices). + + Echo Mini is now auto-detected at `device add` — when the USB descriptor matches the known VID/PID (`0x071b`/`0x3203`), no `--type echo-mini` flag is required. + + `enumerateConnectedDevices` is now the recommended way to discover and classify USB devices. It accepts a `providers: DeviceProvider[]` array and returns `EnumeratedDevice[]` carrying both the USB connection info and the provider-produced identity. + + `getCapabilities` in `@podkit/devices-ipod` is libgpod-free. Capability synthesis is purely table-and-firmware-driven; the legacy `createIpodCapabilities` adapter that depended on a live libgpod `LibgpodDeviceInfo` struct is deprecated in `@podkit/core`. Parity is verified across all 29 generations (the 4 that were libgpod `unknown` degenerate cases are now correctly populated from the table). + + Internal re-export shims in `@podkit/core` keep all existing call paths compiling for one release. The shims delegate to `@podkit/devices-ipod` and `@podkit/devices-mass-storage` and will be removed in P4. + +- [`f61a83b`](https://github.com/jvgomg/podkit/commit/f61a83b3a2d13612730f174759fd3b86edd42e82) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix `podkit device scan` reporting phantom "Unknown iPod (USB only)" entries for non-iPod USB peripherals (mice, hubs, Thunderbolt docks, Ethernet adapters, USB drives). Each phantom suggested `podkit device init` — a destructive operation that could mutate an unrelated device. + + Root cause was architectural: a single `discoverUsbIpods()` function mixed three concerns — USB enumeration, iPod-domain enrichment, and the function name's implied filter (which it didn't actually do). Refactored into clean layers: + - **`@podkit/core` — pure USB enumeration.** `enumerateUsb()` returns `EnumeratedUsbDevice[]` with vendor/product/serial/bus/devnum/diskIdentifier ONLY. No iPod-domain knowledge. + - **`@podkit/devices-ipod` — iPod classifier.** `classifyAsIpod(dev)` returns `IpodClassification | null` (matches Apple-vendor with iPod or iOS PIDs). + - **`@podkit/devices-mass-storage` — mass-storage classifier.** `classifyAsMassStorage(dev)` returns `MassStorageClassification | null` (matches `USB_PRESET_HINTS` entries like Echo Mini). + - **`@podkit/core` composer.** `classifyUsbDevices()` runs both classifiers and returns recognized devices as a tagged union; drops unknown peripherals. + - **CLI `device scan`** now calls `enumerateUsb()` → `classifyUsbDevices()` → renders by `kind`. No domain logic in the command layer. + + Added `kind: 'mass-storage'` rendering branch so mass-storage DAPs are no longer mis-labeled as "Unknown iPod". + + Removed `discoverUsbIpods` and the leaky `UsbDiscoveredDevice` type (which previously carried iPod-domain fields). Adding a new mass-storage device now means adding one entry to `USB_PRESET_HINTS` — no `@podkit/core` change required. Adding a new iPod generation means updating `@podkit/devices-ipod` tables — no other package changes. + + Also split `usb-discovery.ts` into `usb-enumeration.ts` (bus walk) + `usb-path-resolution.ts` (mount-path → fingerprint resolver) since those two concerns were unrelated. + +- Updated dependencies [[`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af), [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b), [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6), [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4)]: + - @podkit/device-types@0.1.0 diff --git a/packages/devices-mass-storage/package.json b/packages/devices-mass-storage/package.json index b7259b9e..e755ff2a 100644 --- a/packages/devices-mass-storage/package.json +++ b/packages/devices-mass-storage/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/devices-mass-storage", - "version": "0.0.1", + "version": "0.1.0", "description": "Mass-storage device presets and types for podkit (Echo Mini, Rockbox, generic DAPs)", "type": "module", "main": "./dist/index.js", diff --git a/packages/docs-site/CHANGELOG.md b/packages/docs-site/CHANGELOG.md index 347a1568..e27fb499 100644 --- a/packages/docs-site/CHANGELOG.md +++ b/packages/docs-site/CHANGELOG.md @@ -1,5 +1,12 @@ # @podkit/docs-site +## 0.0.3 + +### Patch Changes + +- Updated dependencies []: + - @podkit/compatibility@0.0.2 + ## 0.0.2 ### Patch Changes diff --git a/packages/docs-site/package.json b/packages/docs-site/package.json index fbeff8a7..bbce04c1 100644 --- a/packages/docs-site/package.json +++ b/packages/docs-site/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/docs-site", - "version": "0.0.2", + "version": "0.0.3", "dependencies": { "@astrojs/starlight": "^0.33.0", "@podkit/compatibility": "workspace:*", diff --git a/packages/ipod-firmware/CHANGELOG.md b/packages/ipod-firmware/CHANGELOG.md new file mode 100644 index 00000000..b1cad059 --- /dev/null +++ b/packages/ipod-firmware/CHANGELOG.md @@ -0,0 +1,102 @@ +# @podkit/ipod-firmware + +## 0.1.0 + +### Minor Changes + +- [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6) Thanks [@jvgomg](https://github.com/jvgomg)! - P4 — device-capability architecture complete. All in-tree migration finished; deprecated shims removed. + + **Breaking changes in `@podkit/core`:** The following symbols have been removed from the public API: `createIpodCapabilities`, `LibgpodDeviceInfo`, `DEVICE_PRESETS`, `DevicePreset`, `getDevicePreset`, and `resolveDeviceCapabilities`. Callers must migrate before upgrading. + + Migration guide: + - `createIpodCapabilities(libgpodInfo)` → `resolveCapabilities(identity)` (preferred, identity-driven) or `resolveIpodModelCapabilities(modelFromLibgpodInfo(libgpodInfo))` for callers that genuinely hold libgpod data. + - `getDevicePreset(deviceType)` → `BUILT_IN_PRESETS[deviceType]` from `@podkit/devices-mass-storage`. + - `DEVICE_PRESETS` → `BUILT_IN_PRESETS` from `@podkit/devices-mass-storage`. + - `resolveDeviceCapabilities(type, overrides)` → `resolveCapabilities(identity, { overrides })` from `@podkit/core`. + - The `core/device/sysinfo-extended` shim path is gone; import `readSysInfoExtended`, `writeSysInfoExtended`, and `ensureSysInfoExtended` from `@podkit/ipod-firmware` directly. + + See ADR-295.07 for the full architectural rationale. + + **New in `@podkit/ipod-firmware`:** SysInfoExtended file I/O is now owned by this package: `readSysInfoExtended`, `writeSysInfoExtended`, `ensureSysInfoExtended`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR`. Diagnostic helpers `compareSysInfoConsistency` and `normaliseFireWireGuid` are also exported. `ParsedFirmware` gains the optional `modelNumber` field (populated from the `ModelNumStr` plist key when present). + + **New in `@podkit/device-types`:** `IpodModel`, `IpodChecksumType`, `IpodGenerationId`, `IpodGenerationIdLike`, `IpodModelSource`, and `IPOD_GENERATION_IDS` are now exported from this package (canonical home). `UsbConnectionInfo` has been removed — use `UsbFingerprint` instead. `IpodIdentity.notSupportedReason` is added for devices identified as iPods that podkit cannot fully support (e.g. iOS-mode devices). `DeviceCapabilities.artworkMaxResolution` is now `number | null` (null when the generation has no known limit or artwork is unsupported). + + **In `@podkit/devices-ipod`:** `IpodGeneration.supported` and `IpodGeneration.artworkMaxResolution: number | null` are new fields on every generation entry. `lookupByFamilyId` and `FAMILY_ID_TO_GENERATION` are now exported. `unsupported.ts` is populated with comprehensive Apple iOS device PIDs to allow early rejection of phones and tablets at the USB identification stage. + + No user-facing CLI behaviour changes. `podkit device scan`, `podkit device info`, and all sync paths behave identically to P3. + +- [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4) Thanks [@jvgomg](https://github.com/jvgomg)! - Add SCSI firmware inquiry for iPod identification (P1 — m-18 device-capability architecture). + + `@podkit/device-types` (first published release) provides the canonical shared type definitions — `DeviceCapabilities`, `DeviceIdentity`, `ParsedFirmware`, and `DeviceProvider` — used across the podkit monorepo without circular dependencies. + + `@podkit/ipod-firmware` (first published release) implements iPod firmware inquiry via SCSI (Linux SG_IO + macOS IOKit, using koffi FFI) with USB fallback through the existing libgpod-node binding. Devices that previously failed identification over USB — including iPod mini 2G, nano 2G, and some iPod 5G Video configurations — can now be identified via SCSI. The orchestrator probes available transports at startup, prefers USB when both are available, and falls back to SCSI transparently. + + `@podkit/core` now routes `ensureSysInfoExtended` through the new orchestrator with SCSI fallback, and registers two new `podkit doctor` checks: `inquiry-methods` (reports which transports are available on this host) and `sysinfo-consistency` (validates that the on-disk SysInfo file matches the live firmware read). EACCES errors from SCSI include step-by-step recovery instructions. + + `podkit` CLI gains `--repair udev-rule` in `podkit doctor` to install the Linux udev rule that grants non-root `/dev/sg*` access, and surfaces the new doctor checks in the readiness output. + +- [`4598f8f`](https://github.com/jvgomg/podkit/commit/4598f8f3347cf40b94fdf1585215e5b0f54d9cf6) Thanks [@jvgomg](https://github.com/jvgomg)! - USB firmware inquiry consolidated into @podkit/ipod-firmware (P2 — m-18 device-capability architecture). + + **Breaking change in `@podkit/libgpod-node`:** The `readSysInfoExtendedFromUsb` function has been removed from the package's public exports. All in-tree callers were already routed through `@podkit/ipod-firmware` since P1 — only external consumers of `@podkit/libgpod-node` who called this function directly are affected. + + `@podkit/ipod-firmware` now owns the complete firmware inquiry surface: SCSI (Linux SG_IO + macOS IOKit) and USB (direct libusb-1.0 via koffi FFI). The P1 transitional shim that delegated USB reads to libgpod-node has been replaced by a native TypeScript implementation. No API change is visible to callers of `@podkit/ipod-firmware`. + + `@podkit/libgpod-node` no longer requires libusb at build or runtime. Distro packagers can now build the native binding without `libusb-1.0-0-dev` (Debian/Ubuntu), `libusb-devel` (Fedora/RHEL), or equivalent system packages. The `itdb_usb.c` patch, the `dlsym` shim, and the libusb pkg-config dependency have all been removed from the binding. + + No user-facing CLI behaviour changes. `podkit doctor` inquiry checks, `podkit device scan`, and all sync paths behave identically to P1. + +- [`e825ee1`](https://github.com/jvgomg/podkit/commit/e825ee1dd4933ecbfd070dda27f96f43056f0baf) Thanks [@jvgomg](https://github.com/jvgomg)! - Replace koffi-based libusb FFI with the `usb` npm package for USB firmware inquiry, eliminating the runtime libusb system dependency. + + The `@podkit/ipod-firmware` USB transport now uses the `usb` npm package, whose prebuilt N-API bindings statically link libusb. End-user binaries embed that prebuild via Bun `--compile`; no system `libusb-1.0` is required at runtime. + + Public-surface changes in `@podkit/ipod-firmware`: + - **Removed:** `loadLibusb`, `LibusbBinding`, `LibusbPtr`, `LibusbLoadResult`, `_resetLibusbCacheForTests`. The koffi-shaped binding interface is gone. + - **Added:** `loadUsb`, `UsbBinding`, `UsbDeviceHandle`, `UsbLoadResult`, `_resetUsbCacheForTests`. Higher-level `withOpenDevice(bus, devnum, fn)` seam — implementations handle enumeration, open, and cleanup internally. + - **Added:** `setLogger(fn | null)`, `FirmwareLogger`, `FirmwareLogEvent`. Library no longer writes to stderr/stdout; consumers install a receiver and decide format/destination. The CLI installs one when `-v` is passed. + - **Added:** `@podkit/ipod-firmware/bundle` subpath export with `bundleUsbNative(nativeModule)` for single-file binary builds. See `agents/ipod-firmware.md` for the staging recipe. + - **Renamed:** `UsbInquiryError.libusbCode` → `UsbInquiryError.libusbStatus`. The new field carries `LIBUSB_TRANSFER_*` status codes (positive enum) from the `usb` npm package, not the negative `LIBUSB_ERROR_*` codes the koffi path returned. + + Doctor's `inquiry-methods` check no longer reports libusb availability — the USB transport is bundled and always present in shipped binaries. The check now reports SCSI transport availability only, which remains user-actionable on Linux (udev permissions) and macOS (iPodDriver.kext). + +### Patch Changes + +- [`bb2e637`](https://github.com/jvgomg/podkit/commit/bb2e6374151605d11baf052c452f10a842e5353e) Thanks [@jvgomg](https://github.com/jvgomg)! - Externalize `koffi` and `usb` from the published `bun build` bundles. Koffi loads its native binding via `eval('require')(filename)`; bun's bundler shims top-level `require` as `__require` (via `createRequire(import.meta.url)`) but does not inject `require` into eval'd literals, so the bundled CLI hit `ReferenceError: require is not defined` whenever the SCSI inquiry path was actually reached. The native loaders are now resolved at runtime via `node_modules`, which is also more correct for `usb` (whose `bun build`-time prebuild only matched the build host's platform). + + The standalone-binary path (`bun --compile` via `compile.sh`) is unchanged — it stages platform-specific `.node` files and uses static `require()` in `compile-entry.js`, which works correctly. A bug in `compile.sh`'s linux-arm64 branch is also fixed: the script previously constructed `linux-arm64/node.napi.${USB_VARIANT}.node` (where `USB_VARIANT` is `glibc` or `musl`) but the `usb` package only ships `linux-arm64/node.napi.armv8.node` — no glibc/musl split exists for arm64. The script now selects the armv8 prebuild unconditionally on arm64. + + `@podkit/ipod-firmware` is also externalized from the `@podkit/core` and `@podkit/devices-ipod` builds, so neither package's `dist/index.js` re-inlines firmware (and therefore koffi/usb imports). Bundle content-check tests under `packages/*/src/bundle.test.ts` assert that no `eval("require")` slips into any published bundle. + +- [`a78e5fe`](https://github.com/jvgomg/podkit/commit/a78e5fee4e47293c1935395bb157cb6574782625) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix three `podkit doctor` repair correctness bugs: + - `--repair sysinfo-consistency` now overwrites a stale on-disk SysInfoExtended (previously short-circuited on file existence, reporting success without rewriting). + - `--repair sysinfo-extended` no longer requires an existing iTunesDB — repairs without a `database` requirement skip the DB open so identity-populating repairs work on freshly formatted iPods. New `'database'` value on `RepairRequirement`. + - The readiness `SysInfoExtended:` status line distinguishes a missing file from a present-but-unparseable one. + +- [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix iPod model identification regressing to "Unknown iPod" after `doctor --repair sysinfo-extended` on pre-2006 devices (mini 2G), and tighten the package-boundary contract so consumers compose identity instead of injecting resolution policy. + + The bug: each consumer of `ensureSysInfoExtended` / `readSysInfoExtended` passed a serial-only `resolveModel` callback. When the 3-character serial suffix wasn't in `tables/serials.ts`, the resolver returned undefined and the device was displayed as "Unknown iPod" — even when a SysInfo file with a known `ModelNumStr` was sitting next to the SysInfoExtended on disk. + + The fix: + - **Removed** `ModelResolver` type and the `resolveModel` callback from `@podkit/ipod-firmware`. `readSysInfoExtended` and `ensureSysInfoExtended` now return a flat `SysInfoIdentity` bag (`firewireGuid?, serialNumber?, modelNumStr?, familyId?`). When a SysInfo file is on disk alongside SysInfoExtended, its `ModelNumStr` is read opportunistically. + - **Callers compose** with `resolveIpodModel(bag)` from `@podkit/devices-ipod`, which cascades modelNumStr → serial → productId → familyId → libgpodGeneration. The CLI no longer makes resolution decisions. + - **Added** `SYSINFO_PATH`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR` exported from `@podkit/ipod-firmware` and re-exported from `@podkit/core`. Consumers use these constants instead of duplicating the literal `iPod_Control/Device/...` paths. + - **Added** `S4G: '9804'` entry to `tables/serials.ts` (mini 2G 4GB Pink, sourced from real hardware, serial `JQ5141TFS4G`). + - **Post-write enrichment.** After `ensureSysInfoExtended` writes the file via USB inquiry, it now re-reads via `readSysInfoExtended` so the post-write identity bag includes `modelNumStr` from the SysInfo neighbour. Eliminates the cosmetic regression where the repair-success message showed a less-specific name than the subsequent `doctor` run. + +- [`eed4126`](https://github.com/jvgomg/podkit/commit/eed4126fe91ff64f00d74e8a2aaaae38ca6d786b) Thanks [@jvgomg](https://github.com/jvgomg)! - Improve the firmware-inquiry orchestrator's failure message so users can see what went wrong without `-vv`. The default error now names every transport attempted (USB, SCSI) with each one's failure reason on its own line and includes a remediation hint (e.g. `podkit doctor --repair udev-rule` for EACCES on `/dev/sg*` or `/dev/bus/usb/...`). The orchestrator also no longer short-circuits a planned SCSI fallback when USB hits a permission wall — both transports run if the plan calls for it. + +- [`80fe65a`](https://github.com/jvgomg/podkit/commit/80fe65a022c65da512f571a8abf83f9385a649e6) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix SysInfoExtended SCSI-fallback on macOS for SCSI-only iPods (mini 2G, nano 2G, iPod 5G/5.5G). `device add` and `doctor --repair sysinfo-extended` now correctly fall back from USB → SCSI when the device does not respond to vendor control transfers, instead of failing with a misleading "Could not read device identity from USB" error. + + Internal API changes in `@podkit/ipod-firmware`: + - **Changed:** `ensureSysInfoExtended(mountPoint, fp, options?)` now takes a full `UsbFingerprint` instead of the previous `{ busNumber, deviceAddress }` shape. Required so the macOS SCSI transport can locate the IOService via vendorId/productId/serialNumber. `UsbDeviceAddress` is removed. + - **Added:** `inquireFirmwareDetailed(fp, opts?)` — like `inquireFirmware` but returns `{ firmware, plan, attempts }` so callers can distinguish which transports were attempted. `inquireFirmware` is unchanged for existing consumers. + - **Added:** `EnsureSysInfoExtendedOptions` type with `readFromUsb`, `resolveModel`, `inquireOptions` fields. Replaces the previous positional `(mountPoint, fp, readFromUsb, resolveModel)` signature. + + Internal API changes in `@podkit/core`: + - **Added:** `hasCompleteUsbFingerprint(fp): fp is CompleteUsbDevice` type guard exported from `@podkit/core`. + - **Added:** `CompleteUsbDevice` type — a `UsbFingerprint` with vendorId, productId, bus, devnum guaranteed present (serialNumber optional). + - **Changed:** `resolveUsbDeviceFromPath(path)` now also returns `vendorId` and `productId`. Linux extracts from sysfs `idVendor`/`idProduct`; macOS extracts from `system_profiler` JSON. + + User-facing error messages now differentiate between transport failures: "Could not read device identity from USB and SCSI" / "...from USB" / "...from SCSI" / "...no firmware inquiry transport is available on this system" / "...returned data but it could not be parsed". + +- Updated dependencies [[`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af), [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b), [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6), [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4)]: + - @podkit/device-types@0.1.0 diff --git a/packages/ipod-firmware/package.json b/packages/ipod-firmware/package.json index 3e697544..a6f2cb02 100644 --- a/packages/ipod-firmware/package.json +++ b/packages/ipod-firmware/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/ipod-firmware", - "version": "0.0.1", + "version": "0.1.0", "description": "iPod firmware inquiry — SCSI and USB delivery for podkit", "type": "module", "main": "./dist/index.js", diff --git a/packages/libgpod-node/CHANGELOG.md b/packages/libgpod-node/CHANGELOG.md index 1a227715..cc5ea59e 100644 --- a/packages/libgpod-node/CHANGELOG.md +++ b/packages/libgpod-node/CHANGELOG.md @@ -1,5 +1,59 @@ # @podkit/libgpod-node +## 0.2.0 + +### Minor Changes + +- [`22dddf4`](https://github.com/jvgomg/podkit/commit/22dddf4803f4cfd7b004d80dffd83878a68b10f2) Thanks [@jvgomg](https://github.com/jvgomg)! - Add standalone Device class for capability queries without opening a database + - `Device.fromMountPoint(path)` — reads SysInfo from filesystem, determines capabilities + - `Device.fromModelNumber(num)` — cached lookup from model number string, no filesystem needed + - Exposes `supportsArtwork`, `supportsVideo`, `supportsPhoto`, `supportsPodcast`, `generation`, `modelNumber`, `modelName`, `capacity` + - Add `ArtworkFormat` type (reserved for future artwork dimension exposure) + +- [#59](https://github.com/jvgomg/podkit/pull/59) [`1ec30ac`](https://github.com/jvgomg/podkit/commit/1ec30acca1109178012db3913a60967a2087fb5b) Thanks [@jvgomg](https://github.com/jvgomg)! - Automated iPod device identification via SysInfoExtended. + + Modern iPods (post-2006) ship without a populated `SysInfo` file after iTunes restore. Without it, libgpod treats the device as generic — artwork breaks, ALAC support is unknown, and database checksums fail on Classic 6/7G and Nano 3G+. podkit now reads SysInfoExtended directly from iPod firmware over USB during `device add`, so first-time setup works with no manual tooling. + + **User-visible:** + - `podkit device add` identifies the exact model (e.g. "iPod nano 8GB Black (3rd Generation)") with no input + - `podkit doctor` detects missing SysInfoExtended and offers `--repair sysinfo-extended` + - `podkit sync` works correctly on first run with full capability detection + - Hash72 (Nano 5G) and HashAB (Nano 6G) devices get clear limitation messages + - SysInfoExtended write is gated on user confirmation during `device add` + + **Core (`@podkit/core`):** + - Unified iPod model registry — single table, both `0x120x`/`0x126x` USB ID ranges, 190+ serial-suffix → model mappings, checksum-type classification per generation + - `ensureSysInfoExtended()` orchestrator: check existing → USB read → validate XML → write + - USB discovery now exposes `serialNumber`, `busNumber`, `deviceAddress`; `resolveUsbDeviceFromPath()` on macOS + Linux + - Readiness pipeline: checksum-aware severity (hash58+ devices fail without SysInfoExtended; pre-checksum devices warn) + - `READINESS_RULES` declarative array replaces ad-hoc `determineLevel()` logic + - New `sysinfo-extended` diagnostic check + - Recognizes `P` / `F` model prefixes in SysInfo + + **libgpod-node (`@podkit/libgpod-node`):** + - `readSysInfoExtendedFromUsb()` N-API binding, resolved via `dlsym` at runtime so it loads gracefully on systems where libgpod lacks the symbol + - Prebuild patches upstream libgpod 0.8.3 to move `itdb_usb.c` from `tools/` into the library; libusb 1.0.27 built from source on all 6 platforms + - `--whole-archive` / `-force_load` linker flags preserve the dlsym symbol in the `.node` binary + + **CLI (`podkit`):** + - `device add` attempts SysInfoExtended read after mount, before DB init; enriches model name in summary + - `doctor` adds suggested-actions section, drops destructive sysinfo guidance + - `device scan` and `doctor` show clearer SysInfo readout + +- [`4598f8f`](https://github.com/jvgomg/podkit/commit/4598f8f3347cf40b94fdf1585215e5b0f54d9cf6) Thanks [@jvgomg](https://github.com/jvgomg)! - USB firmware inquiry consolidated into @podkit/ipod-firmware (P2 — m-18 device-capability architecture). + + **Breaking change in `@podkit/libgpod-node`:** The `readSysInfoExtendedFromUsb` function has been removed from the package's public exports. All in-tree callers were already routed through `@podkit/ipod-firmware` since P1 — only external consumers of `@podkit/libgpod-node` who called this function directly are affected. + + `@podkit/ipod-firmware` now owns the complete firmware inquiry surface: SCSI (Linux SG_IO + macOS IOKit) and USB (direct libusb-1.0 via koffi FFI). The P1 transitional shim that delegated USB reads to libgpod-node has been replaced by a native TypeScript implementation. No API change is visible to callers of `@podkit/ipod-firmware`. + + `@podkit/libgpod-node` no longer requires libusb at build or runtime. Distro packagers can now build the native binding without `libusb-1.0-0-dev` (Debian/Ubuntu), `libusb-devel` (Fedora/RHEL), or equivalent system packages. The `itdb_usb.c` patch, the `dlsym` shim, and the libusb pkg-config dependency have all been removed from the binding. + + No user-facing CLI behaviour changes. `podkit doctor` inquiry checks, `podkit device scan`, and all sync paths behave identically to P1. + +### Patch Changes + +- [`6000868`](https://github.com/jvgomg/podkit/commit/6000868830d9437a6fff3c1a77adb254d9579fe7) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix `doctor --repair sysinfo-extended` showing unhelpful "Could not read device identity from USB" with no detail. The native USB binding now throws descriptive errors (e.g. "USB control transfer failed (bus 3, device 4)") instead of returning null silently. Also fix all doctor repair intro messages — they incorrectly said "Repairing X for N tracks" even for non-track operations like SysInfoExtended and orphan cleanup. Intro messages now use each repair's own description. + ## 0.1.0 ### Minor Changes diff --git a/packages/libgpod-node/package.json b/packages/libgpod-node/package.json index eef7712f..d85fe73e 100644 --- a/packages/libgpod-node/package.json +++ b/packages/libgpod-node/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/libgpod-node", - "version": "0.1.0", + "version": "0.2.0", "description": "Native Node.js bindings for libgpod", "type": "module", "main": "./dist/index.js", diff --git a/packages/podkit-cli/CHANGELOG.md b/packages/podkit-cli/CHANGELOG.md index 82fa63ec..0e43943a 100644 --- a/packages/podkit-cli/CHANGELOG.md +++ b/packages/podkit-cli/CHANGELOG.md @@ -1,5 +1,917 @@ # podkit +## 0.7.0 + +### Minor Changes + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`0f3e4dd`](https://github.com/jvgomg/podkit/commit/0f3e4ddae134228b5e874b21db33f74547867b6c) Thanks [@jvgomg](https://github.com/jvgomg)! - Add capability-gated clean artists transform + + Devices now declare whether they use Album Artist for browse navigation via `supportsAlbumArtistBrowsing`. When enabled globally, the `cleanArtists` transform is automatically suppressed on devices that support Album Artist browsing (Rockbox, Echo Mini, generic) and auto-applied on devices that don't (iPod). Per-device overrides still take priority. + + The dry-run summary shows when the transform is skipped (`Clean artists: skipped (device supports Album Artist browsing)`), and warns when it's force-enabled on a capable device. Both `sync --dry-run` and `device info` surface these in text and JSON output. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`036b107`](https://github.com/jvgomg/podkit/commit/036b1077748253385b6f4ff873a7cdb52c54b004) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix mass-storage directory structure to use album artist instead of track artist, and add template-based path system with self-healing relocate. + + **Bug fix:** Mass-storage devices (Echo Mini, Rockbox) now use `albumArtist` for directory grouping, falling back to `artist` when absent. Previously, compilation/various-artist albums had their tracks scattered across separate artist directories instead of being grouped together under the album artist. + + **Path templates:** File paths are now generated from a configurable template string (`{albumArtist}/{album}/{trackNumber} - {title}{ext}` by default). This lays the groundwork for user-customisable folder structures in a future release. + + **Self-healing relocate:** When source metadata changes (e.g. album artist corrected) or the path template changes, the next sync detects the path mismatch and moves files to their correct location via `fs.rename()` — no re-copying of audio data. Relocate operations appear in dry-run output and are tracked as a new `relocate` operation type. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`89ff40c`](https://github.com/jvgomg/podkit/commit/89ff40c2adedd9fec38ae5ad0eb89b75525642f2) Thanks [@jvgomg](https://github.com/jvgomg)! - Add audioNormalization device capability for device-appropriate Sound Check / ReplayGain handling + + Devices now declare their normalization support: 'soundcheck' (iPod), 'replaygain' (Rockbox), or 'none' (Echo Mini, generic). Devices with no normalization support skip soundcheck upgrade detection entirely, and the dry-run output hides or relabels the normalization line accordingly. Configurable via `audioNormalization` in device config. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`c5c0236`](https://github.com/jvgomg/podkit/commit/c5c0236c232cc3fa086fd3937b0e2fbe0f326185) Thanks [@jvgomg](https://github.com/jvgomg)! - Refactor audio normalization from iPod-centric Sound Check to a generic `AudioNormalization` type, and add ReplayGain album gain/peak support + + **Normalization refactoring:** + - Introduce `AudioNormalization` type that preserves source format fidelity (ReplayGain dB, iTunNORM soundcheck integers) without unnecessary round-trip conversions + - Replace scattered `soundcheck`, `soundcheckSource`, `replayGainTrackGain`, `replayGainTrackPeak` fields on `CollectionTrack` with a single `normalization` field + - Replace `soundcheck`, `replayGainTrackGain`, `replayGainTrackPeak` fields on `DeviceTrackInput` with `normalization` + - Conversions now happen at device boundaries: iPod adapter reads soundcheck integers, mass-storage adapter reads dB values directly + - Upgrade detection compares in dB space with 0.1 dB epsilon tolerance, eliminating false positives from integer rounding + - Metadata update diffs show human-readable dB values (e.g., `normalization: -7.5 dB → -6.2 dB`) instead of opaque integers + + **Album gain/peak support (TASK-253):** + - Extract `albumGain` and `albumPeak` from local file metadata and Subsonic API + - Write `REPLAYGAIN_ALBUM_GAIN` and `REPLAYGAIN_ALBUM_PEAK` via FFmpeg metadata flags during transcode + - Write album gain/peak via node-taglib-sharp tag writer for M4A files + - Thread album data through the full sync pipeline for mass-storage devices (Rockbox, etc.) + + **Breaking changes:** + - `CollectionTrack` shape: four normalization fields replaced by single `normalization?: AudioNormalization` + - `SoundCheckSource` type removed, replaced by `NormalizationSource` + - Upgrade reason `'soundcheck-update'` renamed to `'normalization-update'` in JSON output + - `soundCheckTracks` stat renamed to `normalizedTracks` + +- [`ae995ef`](https://github.com/jvgomg/podkit/commit/ae995ef99174f7381f4eeaeb79cee4e77ddc3136) Thanks [@jvgomg](https://github.com/jvgomg)! - Unify JSON error shape for `device add` and `collection music`/`collection video` + + These commands now emit the same JSON error format on failure: + + ```json + { + "success": false, + "error": "", + "code": "", + "...details": "" + } + ``` + + **Breaking for `collection music` / `collection video` JSON consumers.** The previous shape was `{ "error": true, "message": "..." }`. If you parse JSON output from these commands, update consumers to read `success === false` and `error` (instead of `error === true` and `message`). + + `device add` errors now also include a `code` field (additive, not breaking). + + Underneath: the runners (`runDeviceAdd`, `runCollectionMusic`, `runCollectionVideo`) throw a typed `CliError` and the action wrapper (`runAction`) translates it into structured output + exit code. Tests assert on the captured JSON instead of `process.exitCode` side-effects. + + Per CLI breaking-change convention this is a minor bump. Other commands still emit their existing shapes; that unification will land in a follow-up. + +- [`8c6dc1a`](https://github.com/jvgomg/podkit/commit/8c6dc1a3d355efe6542a7f00f8fa05da3225bb42) Thanks [@jvgomg](https://github.com/jvgomg)! - Unify and harden CLI JSON error output across every command (ADR-015) + + ## What changed + + Every CLI command now emits the same canonical JSON shape on failure. The shape, exit codes, and consumer ergonomics all changed in one breaking pass. + + ### Canonical error shape + + ```json + { + "success": false, + "error": "", + "code": "", + "details": { "": "..." } + } + ``` + + `code` is required and machine-readable (e.g. `MOUNT_REQUIRES_SUDO`, `FFMPEG_UNAVAILABLE`). `details` is **nested**, not spread at the top level — so command-specific extras can't accidentally collide with `success`/`error`/`code`. + + ### Exit codes + - `0` — success + - `1` — command error (any `CliError` thrown) + - `2` — ran cleanly but found problems (`doctor` reporting unhealthy device, `sync` reporting partial track failures). Carries a `status` field on the success-shape JSON: `'ok' | 'issues-found' | 'partial-failure'`. + - `130` — SIGINT (interrupted sync) + + ### Per-command typed error codes + + Every command exports an exhaustive enum of its possible error codes: + + ```ts + import { MountErrorCodes, type MountErrorCode } from 'podkit/commands/mount'; + // MountErrorCodes.DEVICE_NOT_RESOLVED, MountErrorCodes.MOUNT_REQUIRES_SUDO, etc. + ``` + + A repo-wide barrel is at `packages/podkit-cli/src/commands/error-codes.ts` exporting `PodkitErrorCode` — the union of every code any podkit command may emit. + + ### Discriminated `*Output` types + + Each command's output type is now a discriminated union: + + ```ts + export type MountOutput = MountSuccess | MountErrorOutput; + ``` + + Consumers narrow with `if (output.success) { ... }`. + + ## Breaking for JSON consumers + + | Old shape | New shape | + | -------------------------------------------------------------- | ------------------------------------------------- | + | `{ error: true, message: "..." }` (collection music/video) | `{ success: false, error, code, details }` | + | `{ success: false, error: "..." }` (no code, top-level extras) | `{ success: false, error, code, details: {...} }` | + | Per-command extras at top level (e.g. `dryRun`, `device`) | Now nested under `details` | + | `process.exitCode === 1` for "found issues" | Now `2`; `1` is reserved for command errors | + + Update parsers to: + 1. Branch on `success === false`. + 2. Read `code` for machine-readable tags. + 3. Read `details.X` instead of `output.X` for command-specific extras. + 4. Branch on exit code 2 for "ran cleanly with issues" (sync partial failure, doctor unhealthy). + + ## New ergonomics + + `packages/podkit-cli/src/test-utils/cli-error.ts` and `test-packages/e2e-tests/src/helpers/cli-error.ts` export `expectCliError` for asserting on the canonical shape in one call. + + `OutputContext` now takes an optional `ExitCodeSink` (default: writes `process.exitCode`; tests use `BufferExitCodeSink` to avoid process-global mutation). + + Per CLI breaking-change convention this is a minor bump. + +- [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af) Thanks [@jvgomg](https://github.com/jvgomg)! - Disambiguate codec from container: `AudioCodec` value `'ogg'` is renamed to `'vorbis'` + + The `AudioCodec` slot previously used `'ogg'` to mean "OGG Vorbis." That conflated the OGG container with the Vorbis stream codec and could not represent Vorbis-in-OGG vs Opus-in-OGG as distinct device capabilities — Echo Mini (which plays Vorbis but hides `.opus` files) could not be modelled accurately. The codec slot now names the audio stream codec; `'vorbis'` replaces `'ogg'` in device presets and config. + + Configs containing `supportedAudioCodecs = ["…", "ogg", "…"]` under `[devices.*]` are migrated automatically by `podkit migrate` (config version 1 → 2). The migration is purely a string substitution inside the `supportedAudioCodecs` array; comments and surrounding formatting are preserved. + + Also lands as type-level groundwork for the future container-aware sync work: `AudioContainer`, `AUDIO_CONTAINERS`, `CODEC_CANONICAL_CONTAINER`, and an optional `DeviceCapabilities.containerConstraints` field. These are declared and exported but not yet read by the planner; they are placeholders for the upcoming Phase 2 work documented in the container-aware sync PRD. + + `DirectoryAdapter` now uses each `.ogg` file's probed stream codec (already populated by `music-metadata`) to distinguish Vorbis, Opus, and OGG-FLAC — same pattern as the existing AAC/ALAC distinction for `.m4a`. `SubsonicAdapter` additionally checks the API's `contentType` field for Opus-in-`.ogg`. The Subsonic check is best-effort because most Subsonic servers report container MIME (`audio/ogg`) regardless of stream codec; deeper probing is deferred until evidence of real-world impact. + + User-facing reference page added at `docs/reference/codec-support.md` explaining the codec/container model and what each `AudioCodec` value means. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`513173d`](https://github.com/jvgomg/podkit/commit/513173d1832bf9ca2894214e97d9d65cf02c52a5) Thanks [@jvgomg](https://github.com/jvgomg)! - Add configurable codec preference system for multi-device audio format support + + Users can now configure an ordered list of preferred audio codecs globally and per-device. The system walks the list top-to-bottom, selecting the first codec that is both supported by the target device and has an available FFmpeg encoder. This replaces the hardcoded AAC-only transcoding pipeline. + - **Default lossy stack:** opus → aac → mp3 (Rockbox devices get Opus automatically, iPods fall through to AAC) + - **Default lossless stack:** source → flac → alac (lossless files are kept in their original format when possible) + - **Quality presets are codec-aware:** "high" delivers perceptually equivalent quality regardless of codec (e.g., Opus 160 kbps ≈ AAC 256 kbps) + - **Codec change detection:** changing your codec preference re-transcodes affected tracks on the next sync + - **`podkit device info`** shows your codec preference list with supported/unsupported codecs marked + - **`podkit sync --dry-run`** shows which codec will be used and any codec changes + - **`podkit doctor`** warns when FFmpeg is missing an encoder for a preferred codec + + Configure via `config.toml`: + + ```toml + [codec] + lossy = ["opus", "aac", "mp3"] + lossless = ["source", "flac", "alac"] + + [devices.myipod.codec] + lossy = "aac" + ``` + + No configuration is required — existing setups work unchanged with sensible defaults. + +- [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b) Thanks [@jvgomg](https://github.com/jvgomg)! - Consolidate the two ways podkit expressed "this device is unsupported" into one canonical shape. `ReadinessUnsupportedReason` moves to `@podkit/device-types` (its natural home), and `resolveIpodModel(bag)` now returns it directly on `IpodModel.unsupportedReason` instead of the bare-string `notSupportedReason`. The bridge functions in `@podkit/core` (`makeUnsupportedReasonFromModel`, `makeUnsupportedReasonFromAssessment`) are removed — consumers read `model.unsupportedReason` directly. Internal refactor; user-facing CLI behaviour is unchanged. + +- [`0cc39d3`](https://github.com/jvgomg/podkit/commit/0cc39d3c62343591127d5c79deed2478f8dc4f60) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix track metadata convergence on mass-storage devices and add transfer-mode-aware on-disk tag writes for iPod portable. + + **Bug fix (mass-storage)**: `MassStorageAdapter.updateTrack` previously only wrote `comment`, OGG/Opus artwork, and ReplayGain to disk. All other metadata fields (title, artist, album, albumArtist, genre, year, trackNumber, discNumber, compilation) updated in-memory only — the file's embedded tags on the device were never rewritten. After a relocate or metadata-correction sync the next sync re-detected the same diff every time, looping forever as a zero-byte `update-metadata` op. + + `MassStorageAdapter` now queues every changed textual tag in a single `pendingTagWrites` map and flushes them as one `writeTags(filePath, fields)` call per file via `Promise.allSettled`. Per-file failures are aggregated and re-thrown so the sync executor can categorise them. + + **New behaviour (iPod portable)**: `IpodDeviceAdapter` now mirrors iTunesDB metadata into the on-disk file tags when `transferMode === 'portable'`. This makes files pulled off the iPod self-describing for re-import into a music library. `fast` and `optimized` modes still touch iTunesDB only — the iPod firmware reads metadata from iTunesDB and never falls back to file tags during playback, so paying the tag-rewrite cost in those modes would be wasted work. + + Tag writes are best-effort on iPod portable: failures are surfaced as warnings, not hard errors, because the iTunesDB write (the authoritative store for playback) already succeeded. + + **`addTrack` consistency**: When `transferMode === 'portable'`, both backends now also rewrite tags on first transfer to honour any collection-adapter transforms (e.g. clean-artists, Subsonic-side corrections) that FFmpeg's `-map_metadata 0` would otherwise copy through from the source. + + **On first sync after upgrade**: Existing mass-storage tracks will likely report a `metadata-correction` op on the next sync as stale on-disk tags converge to source values. These are zero-byte writes — no transcoding or transfers happen — but the operation list will look longer than usual for one cycle. + + **Scope notes**: + - Match-key changes (title, artist, album corrections) still produce a remove+add rather than a metadata update. By design: when those fields change, podkit treats it as a different track. + - Virtual-iPod (m-17) inherits the iPod behaviour automatically; no changes needed there. + +- [`484fb0e`](https://github.com/jvgomg/podkit/commit/484fb0ea63eea297f19217d1acb96163a6754b05) Thanks [@jvgomg](https://github.com/jvgomg)! - Cross-process sync coordination: per-device lock, transcode owner-liveness, phantom auto-prune. + + **Per-device sync lock.** `podkit sync` now acquires a per-device PID-file lock at `.podkit/sync.lock` (mass-storage) or `iPod_Control/.podkit-sync.lock` (iPod) immediately after opening the device. A second concurrent `podkit sync` against the same device exits with the new `LOCK_HELD` code (exit code **4**) and a message naming the holding PID: + + ``` + Error: Another podkit process is already syncing /Volumes/TERAPOD (pid 12345). Wait for it to finish or kill it. + ``` + + Crash-safe: the lock file is unlinked in `finally`; if a process is SIGKILLed mid-sync, the next attempt detects the dead PID via liveness probe and takes over cleanly. `podkit sync --dry-run` does **not** take the lock (read-only by design). The daemon (`@podkit/daemon`) detects `LOCK_HELD` from the CLI subprocess and skips that cycle (no retry-spin). Read commands (`device scan`, `device info`, `device music`) are unaffected — writes only. + + **Transcode-tmp owner-liveness.** Each `podkit-transcode-/` scratch directory now writes a small `.owner` file at creation. The pre-sync sweep walker reaps dirs whose owner is dead or whose `.owner` is missing; live owners are left alone. This replaces the previous session-start-time floor, which missed the most common interruption case for the long-running daemon: its own prior cycle. + + **Phantom manifest auto-prune.** When the pre-sync sweep detects manifest rows whose backing audio file has vanished (mass-storage devices), it now prunes them atomically as part of the sweep. Previously the sweep emitted an advisory warning recommending `podkit doctor --repair orphan-files`; the advisory now fires only if the auto-prune itself fails. `doctor --repair orphan-files` remains as a backstop and is unchanged. + + **Filesystem support.** The shared liveness primitive uses only `O_CREAT|O_EXCL`, `unlink`, `rename`, `readFile`, `writeFile`, and `process.kill(pid, 0)` — stable across exFAT, FAT32, HFS+, APFS, ext4, and NTFS. We deliberately avoided `flock(2)` because its semantics on FAT-family filesystems (common for iPods) are platform-dependent. + + **Internal API additions** (`@podkit/core`): `acquireLock`, `LockHandle`, `LockHeldError`, `LockContestedError`, `getOwnIdentity`, `readOwnership`, `writeOwnership`, `isAlive`, `PidFileEntry` — all from a new `lib/pid-file.ts` primitive. `DeviceAdapter` gains an optional `prunePhantomManifest?(paths)` method (implemented on mass-storage, intentionally omitted on iPod). + + **Removed internals:** `SESSION_START_MS` constant and `sessionStartMsOverride` plumbing in `pre-sync-sweep.ts` are gone — replaced wholesale by the PID-file liveness probe. + + See `documents/architecture/sync/planning.md` §6 for the cross-process coordination design. + +- [`59726b1`](https://github.com/jvgomg/podkit/commit/59726b1f63e54afb02cef85d6f58755677b881ee) Thanks [@jvgomg](https://github.com/jvgomg)! - **Behavioural change:** `podkit device add` now refuses to persist a device when the identity cascade resolves nothing at all — no SysInfoExtended path, no classic SysInfo on disk, and no USB fingerprint. Previously such devices were silently persisted with empty identity, which stranded subsequent commands (`podkit doctor -d `, `podkit sync -d `) that rely on identity to track the device across replug cycles. + + The refusal exits with code `1`, prints an actionable error (`EMPTY_IDENTITY`), and points to three remediation paths: + - Re-mount the device read-write and check the USB connection, then retry + - Pass `--no-firmware-inquiry` if you knowingly want to skip the firmware inquiry step + - Pass the new `--force` flag to add the device anyway with a warning + + Partial-cascade scenarios (SysInfoExtended present but USB fingerprint unresolved, or similar) continue to proceed silently — the warning is reserved for cases where neither SysInfoExtended nor classic SysInfo can be read, which is the genuinely actionable signal. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`7534c2f`](https://github.com/jvgomg/podkit/commit/7534c2f19d81087413af8abbf764fe20cef61384) Thanks [@jvgomg](https://github.com/jvgomg)! - Add device-aware diagnostics framework to `podkit doctor`. The doctor command now handles mass-storage devices gracefully instead of crashing when pointed at a non-iPod device. Diagnostic checks declare which device types they apply to, and the runner filters them automatically. JSON output now includes a `deviceType` field. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`8bc3126`](https://github.com/jvgomg/podkit/commit/8bc3126ec415aa836b746ec921b6738abdd9e538) Thanks [@jvgomg](https://github.com/jvgomg)! - Enhanced device commands with readiness diagnostics + - `device scan`: verbose readiness output with per-stage checks, USB discovery for unpartitioned devices, config relationship display, `--mount` flag for automatic mounting, `--report` flag for diagnostic reports + - `podkit doctor`: two-phase diagnostics — readiness checks before database health, graceful handling of devices without databases + - `device info`: readiness summary line in output + - `device init`: readiness-aware guidance with stub messages for format/partition operations + - OS error codes (errno 71, 13, 19, 5) translated to plain-language explanations + +- [`7d7a429`](https://github.com/jvgomg/podkit/commit/7d7a4294b81b42f03143edbad62e746633f0e4d9) Thanks [@jvgomg](https://github.com/jvgomg)! - `DeviceConfig.unsupported` (the marker for devices the user added via the warn-allow flow in TASK-317.03) is now a structured object (`{ kind, confirmedAt }`) instead of a bare boolean. The `kind` captures which unsupported-reason class triggered the prompt (iOS device, hashAB nano, mass-storage with no preset, etc.) so a future reader can tell why the device was confirmed. The `confirmedAt` ISO timestamp records when. Legacy `unsupported = true` config entries are silently coerced to the new shape on load. + +- [`eb0b215`](https://github.com/jvgomg/podkit/commit/eb0b21527a09c71542f6e860876fdf0e763e3f41) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit device info` now distinguishes the "device firmware can play" codec list from the "podkit will write" list on mass-storage devices. When the two disagree (e.g. rockbox declares `wav`/`aiff` but podkit transcodes them before transfer), the capabilities block expands into a `Firmware:` / `Podkit:` sub-block with the gap codecs annotated as transcoded: + + ``` + Capabilities: + Audio Codecs: + Firmware: aac, alac, mp3, flac, vorbis, opus, wav, aiff + Podkit: aac, alac, mp3, flac, vorbis, opus + (wav, aiff transcoded before transfer) + ``` + + When the two lists agree (echo-mini, generic), the existing single `Audio Codecs:` line is preserved. JSON output gains a `status.massStorageCapabilities.firmwareSupportedAudioCodecs` field, omitted when there is no diff (absence signals the two views are equal). iPod output is unchanged. + +- [`8d017e8`](https://github.com/jvgomg/podkit/commit/8d017e8ede48c98b9a1d1c627b882689c33da61e) Thanks [@jvgomg](https://github.com/jvgomg)! - Redesigned `podkit device list` output with resolved config values and provenance tracking + - Shows resolved quality, audio, video, and artwork settings per device with inheritance indicators + - Global config line shows top-level resolved values + - Connected devices detected automatically and marked with ● prefix + - Devices sorted by connection status, then default, then alphabetical + - Values explicitly set on a device shown without brackets; inherited values wrapped in [brackets] + - Unsupported capabilities shown as ✗, unknown (disconnected iPod) shown as ? + - TYPE column replaces VOLUME column + - New config resolution module (`config/resolve.ts`) with `ResolvedValue` provenance tracking + - `device scan` "Configured devices" section renamed to "Not detected" and now includes iPod devices + +- [`6747667`](https://github.com/jvgomg/podkit/commit/6747667049cd793fdb13e3d1bc1092651f8e969c) Thanks [@jvgomg](https://github.com/jvgomg)! - Improve device command output: USB model in scan, SysInfo mismatch detection, summary/issues layout + - `podkit device scan` now shows the USB-detected iPod model (e.g., "iPod Classic 6th generation (USB)") and always runs USB discovery in parallel with disk scanning + - `podkit device scan` and `podkit doctor` detect generation mismatches between SysInfo and USB data, warning when the SysInfo file may have been copied from a different device + - `podkit device info`, `podkit device scan`, and `podkit doctor` now separate compact check summaries from detailed issue explanations — warnings and fix commands appear in a dedicated "Issues" section instead of inline + - New `lookupGenerationByModelNumber()` function in `@podkit/core` for resolving iPod generation from SysInfo model numbers + +- [`14458fd`](https://github.com/jvgomg/podkit/commit/14458fd3454629e33c9f4894e899e7f3e47e5cf5) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit device scan --format json`: rename `notSupportedReason: string` to `unsupportedReason: ReadinessUnsupportedReason` on USB-only device entries + + The JSON envelope for `device scan` previously carried unsupported-device + diagnostics as a bare `notSupportedReason` string. It now matches the structured + `ReadinessUnsupportedReason` shape already used by the readiness pipeline and + `IpodModel.unsupportedReason`: + + ```json + { + "unsupportedReason": { + "kind": "ios-device", + "headline": "iPod Touch is not supported by podkit.", + "docsUrl": "https://jvgomg.github.io/podkit/devices/supported-devices/" + } + } + ``` + + Consumers reading `device.notSupportedReason` should read + `device.unsupportedReason.headline` instead — the same string, just nested + under the typed payload. The change applies to both USB-only iPod entries + (touch, iPhone, iPad, nano 6G/7G, shuffle 3G/4G) and to vendor-recognised + mass-storage devices with no matching preset. + + The same rename also lands on the internal `IpodIdentity` and + `IpodClassification` shapes, but those are not part of the public CLI surface. + +- [`01ecedd`](https://github.com/jvgomg/podkit/commit/01ecedde623ff99e94c5cbda75ff9f9c9ecef632) Thanks [@jvgomg](https://github.com/jvgomg)! - New packages: `@podkit/devices-ipod` (canonical home for iPod generation tables, model lookups, and capability synthesis) and `@podkit/devices-mass-storage` (user-extensible DAP preset framework for Echo Mini, Rockbox, generic, and custom devices). + + Echo Mini is now auto-detected at `device add` — when the USB descriptor matches the known VID/PID (`0x071b`/`0x3203`), no `--type echo-mini` flag is required. + + `enumerateConnectedDevices` is now the recommended way to discover and classify USB devices. It accepts a `providers: DeviceProvider[]` array and returns `EnumeratedDevice[]` carrying both the USB connection info and the provider-produced identity. + + `getCapabilities` in `@podkit/devices-ipod` is libgpod-free. Capability synthesis is purely table-and-firmware-driven; the legacy `createIpodCapabilities` adapter that depended on a live libgpod `LibgpodDeviceInfo` struct is deprecated in `@podkit/core`. Parity is verified across all 29 generations (the 4 that were libgpod `unknown` degenerate cases are now correctly populated from the table). + + Internal re-export shims in `@podkit/core` keep all existing call paths compiling for one release. The shims delegate to `@podkit/devices-ipod` and `@podkit/devices-mass-storage` and will be removed in P4. + +- [`667d66b`](https://github.com/jvgomg/podkit/commit/667d66b90e0979aaff381968358f2cfc78c8e581) Thanks [@jvgomg](https://github.com/jvgomg)! - Refactor the diagnostic-check scope model from a 2-field shape (`scope: 'system' | 'device'` + `category?: 'readiness' | 'database'`) to a single required 3-way union (`scope: 'system' | 'device-readiness' | 'database-health'`). Compile-time enforcement that every check declares which section it renders into; no more silent fallback when `category` is missing. The user-facing CLI `--scope` flag values are unchanged. + +- [`03f1046`](https://github.com/jvgomg/podkit/commit/03f1046b70898b0282d0c96927bca60ee0d55eeb) Thanks [@jvgomg](https://github.com/jvgomg)! - Add `podkit doctor --repair artwork-reset` to clear all artwork from an iPod without needing a source collection. This is a fast alternative to a full rebuild — useful when your source collection isn't available or you just want to clear corrupted artwork quickly. + + Rename `--repair artwork-integrity` to `--repair artwork-rebuild` to better describe what the repair does. The old name no longer works. + +- [`78b0c71`](https://github.com/jvgomg/podkit/commit/78b0c71b9866306aecbb96f2a0e372a86564f2fc) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit doctor` now renders a consistent `System` / `Device Readiness` / `Database Health` section structure across all device types. Previously, mass-storage devices (Echo Mini) collapsed everything into a single `Device Health` bucket and mis-categorised three system-scope checks. The fix audits every check's `scope` tag, adds a `category?: 'readiness' | 'database'` discriminator so device-scope checks can be routed to the right subsection, and skips `iPod Firmware Inquiry Methods` on non-iPod devices. + +- [`14d83e5`](https://github.com/jvgomg/podkit/commit/14d83e5e59eb0a8a801850de775f9fdb4c0e7aa9) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit doctor` gains `--no-system` to skip system-scope checks (FFmpeg encoders, libusb availability, udev rule). System checks remain on by default; pass `--no-system` for device-only diagnostics or in tests where the host environment shouldn't influence the result. + + The `sysinfo-consistency` check is redesigned: a missing `SysInfoExtended` file is now `skip` (not `fail`) since absence is not a failure mode. When the file is present it's compared against the live device on two independent axes — FireWireGUID and model generation — and only fails when at least one axis can be evaluated and disagrees. The check picks up live device data via the new `liveIdentity` field on `DiagnosticContext`, which `runDiagnostics` accepts as part of `RunDiagnosticsInput`. + +- [`e0f65f4`](https://github.com/jvgomg/podkit/commit/e0f65f4b0cf4fce28138849b7a85f2c3a7c1a613) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit doctor` repairs now acquire the per-device sync lock before mutating the device. + + Previously, `podkit doctor --repair ` would happily run while a `podkit sync` (or daemon-driven sync) was mid-flight against the same device. For mass-storage devices, this meant doctor's `--repair orphan-files` could prune phantom manifest entries from `state.json`, only for sync's eventual `save()` to clobber the prune from in-memory state — silently undoing the user's repair. For iPod devices, concurrent libgpod writes (artwork rebuilds, sysinfo fixes, debris cleanup) could corrupt the iTunesDB. + + The fix: every `--repair` that mutates the device now acquires the same per-device lock that `podkit sync` takes (`.podkit/sync.lock` for mass-storage, `iPod_Control/.podkit-sync.lock` for iPod). On contention, doctor exits with `LOCK_HELD` (exit code **4**) and a message naming the holding PID: + + ``` + Error: Another podkit process is using /Volumes/TERAPOD (pid 12345). Wait for it to finish or kill it. + ``` + + Audited and locked: `orphan-files`, `artwork-rebuild`, `artwork-reset`, `debris-files` (iPod), `sysinfo-extended`, `sysinfo-consistency`, `sysinfo-modelnum-mismatch`. System-only repairs that don't touch the device (`udev-rule`, `debris-transcode-tmp`) correctly skip the lock. `--dry-run` repair invocations also skip the lock — dry-run is read-only by design and matches `podkit sync --dry-run`'s policy. + + **Internal:** `resolveSyncLockPath` moved from the CLI to `@podkit/core` (exported from `lib/sync-lock-path.ts`) so doctor and sync share the same implementation. New JSDoc on `pruneManifestRows` documents the lock requirement for any future direct caller. Architecture doc `documents/architecture/sync/planning.md` §6 now enumerates every manifest-writer surface with confirmed lock semantics. + +- [`5b0279c`](https://github.com/jvgomg/podkit/commit/5b0279cc620ed5101351494dc1ea4a709791d0db) Thanks [@jvgomg](https://github.com/jvgomg)! - Add `podkit doctor --scope ` for running host-environment checks without a registered device. + + `--scope system` skips device resolution entirely and runs only the system-scope checks (FFmpeg, codec encoders, video encoder, libgpod runtime, SCSI inquiry, udev rule on Linux). Useful before plugging an iPod in for the first time, and required by the m-19 Tier-3 test harness to assert host-state against a captured `SystemState` fixture. + + `--scope device` requires `-d/--device` and runs only device-scope checks. `--scope all` (default) preserves the existing combined output byte-for-byte; the legacy `--no-system` flag still applies in that mode. JSON output under `--scope system` uses a discriminator field (`scope: "system"`) so consumers can distinguish the two envelopes. + +- [`4efa15c`](https://github.com/jvgomg/podkit/commit/4efa15c7e42874e9dd88ef2731230d5314d83f20) Thanks [@jvgomg](https://github.com/jvgomg)! - Unify `--repair` IDs across device types and add debris-only diagnostic checks. + + The `podkit doctor --repair` flag now uses one ID per repair regardless of device type. Internally, the framework dispatches the right walker based on the connected device: + - `--repair orphan-files` — works on both iPod and mass-storage. (Previously `orphan-files` was iPod-only; `orphan-files-mass-storage` was the mass-storage variant.) + - `--repair debris-files` (new) — cleans podkit's own `.podkit-tmp` and adapter-failure write residue from prior interrupted syncs. Repair is safe-by-design (no confirmation prompt) because every debris file is incomplete by construction. + - `--repair debris-transcode-tmp` (new) — reaps abandoned `podkit-transcode-*` scratch directories from SIGKILLed prior syncs. Uses an mtime-based safety floor so concurrent sibling processes are never disturbed. + + **Breaking:** `--repair orphan-files-mass-storage` has been **removed**. Users running this flag will see Commander's choices() validation error listing the new public IDs (including `orphan-files`). Migration is mechanical: replace every occurrence with `--repair orphan-files`. + + The orphan check no longer reports debris in its detail output — that's the new `debris-files` check's job. Same FS walk, two checks; no double traversal. + +- [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4) Thanks [@jvgomg](https://github.com/jvgomg)! - Add SCSI firmware inquiry for iPod identification (P1 — m-18 device-capability architecture). + + `@podkit/device-types` (first published release) provides the canonical shared type definitions — `DeviceCapabilities`, `DeviceIdentity`, `ParsedFirmware`, and `DeviceProvider` — used across the podkit monorepo without circular dependencies. + + `@podkit/ipod-firmware` (first published release) implements iPod firmware inquiry via SCSI (Linux SG_IO + macOS IOKit, using koffi FFI) with USB fallback through the existing libgpod-node binding. Devices that previously failed identification over USB — including iPod mini 2G, nano 2G, and some iPod 5G Video configurations — can now be identified via SCSI. The orchestrator probes available transports at startup, prefers USB when both are available, and falls back to SCSI transparently. + + `@podkit/core` now routes `ensureSysInfoExtended` through the new orchestrator with SCSI fallback, and registers two new `podkit doctor` checks: `inquiry-methods` (reports which transports are available on this host) and `sysinfo-consistency` (validates that the on-disk SysInfo file matches the live firmware read). EACCES errors from SCSI include step-by-step recovery instructions. + + `podkit` CLI gains `--repair udev-rule` in `podkit doctor` to install the Linux udev rule that grants non-root `/dev/sg*` access, and surfaces the new doctor checks in the readiness output. + +- [`09c4acd`](https://github.com/jvgomg/podkit/commit/09c4acdec349f200a649b2db15fe05345e380a7b) Thanks [@jvgomg](https://github.com/jvgomg)! - Add canonical IpodModel type for structured device identity + - Add `IpodModel` interface — canonical representation of identified iPod model with `displayName`, `generationId`, `checksumType`, `color`, `capacityGb`, `modelNumber`, and `source` provenance + - Add `resolveIpodModel()` factory — builds an `IpodModel` from USB product ID, SysInfo model number, or serial number suffix + - Add `UsbConnectionInfo` interface — pure USB bus topology data, split from device identity + - Restructure `UsbDiscoveredDevice` to carry `usb: UsbConnectionInfo` + `model?: IpodModel` + - Add `usbModel` and `deviceModel` to `ReadinessResult` — USB-derived and SysInfo-derived models kept separate for mismatch detection + - Update `SysInfoExtendedResult` with structured `model`, `firewireGuid`, `serialNumber` fields + - Clean `checkSysInfo()` return type — new `SysInfoCheckResult` separates stage result from device model + - Add `model` to JSON output for `device scan` and `device info` commands + - `device scan` and `device info` now display richest available model name (color/capacity from SysInfo when available) + - Remove `UsbDeviceInfo` type (replaced by `UsbConnectionInfo` + `IpodModel`) + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`94c85d2`](https://github.com/jvgomg/podkit/commit/94c85d2a9d6c85875432a0ebecab540a9ebd67d7) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix `--delete` to only remove managed files on mass-storage devices, and add orphan file detection via `podkit doctor`. + + **Bug fix:** `--delete` previously removed all unmatched files on mass-storage devices, including user-placed files. It now only removes files that podkit manages (tracked in `.podkit/state.json`), matching iPod behavior where only database tracks are candidates for deletion. + + **Collision detection:** Sync now detects when a planned file write would collide with an existing unmanaged file and reports the conflict before writing. Works in both normal sync and `--dry-run` mode. + + **New diagnostic check:** `podkit doctor` now runs health checks on mass-storage devices. The `orphan-files-mass-storage` check detects unmanaged files in content directories and can clean them up via `podkit doctor --repair orphan-files-mass-storage`. + + **Other improvements:** + - State manifest (`.podkit/state.json`) is now written without pretty-printing to reduce file size on device storage + - Shell completions now include valid repair IDs for the `--repair` option + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`efa14c6`](https://github.com/jvgomg/podkit/commit/efa14c623e7bda81066bd77142cddb28e4de615d) Thanks [@jvgomg](https://github.com/jvgomg)! - Add mass-storage device support for non-iPod portable music players. + + **Supported device types:** Echo Mini, Rockbox, and generic mass-storage DAPs. iPod support is unchanged. + + **New in CLI (`podkit`):** + - `podkit device add --type ` registers mass-storage devices by type and mount path + - `podkit device info/music/video` work with mass-storage devices via `DeviceAdapter` interface + - `podkit device scan` shows configured path-based devices alongside auto-detected iPods + - `podkit sync` routes to the correct adapter (iPod or mass-storage) based on device config + - Video sync now uses capabilities-based gating instead of iPod-only checks + - Safety gates on `device init/reset/clear` (iPod-only commands) for mass-storage devices + - Mount and eject commands show device-appropriate messaging + - Config validation rejects capability overrides on iPod devices (capabilities are auto-detected from generation) + - Shared `openDevice()` function eliminates duplicated device-opening logic across commands + + **New in core (`@podkit/core`):** + - `DeviceAdapter` interface — generic abstraction over device databases (iPod, mass-storage) + - `MassStorageAdapter` — filesystem-based track management with `.podkit/state.json` manifest + - `IpodDeviceAdapter` — thin wrapper making `IpodDatabase` implement `DeviceAdapter` + - Device capability presets for Echo Mini, Rockbox, and generic devices + - `resolveDeviceCapabilities()` merges preset defaults with user config overrides + - `DeviceTrack` type used throughout sync engine (replaces `IPodTrack` casts in execution paths) + - Configurable content path prefixes (`musicDir`, `moviesDir`, `tvShowsDir`) with device-type defaults + - Device presets include default content paths (Echo Mini: root for music; generic/Rockbox: `Music/`, `Video/Movies/`, `Video/Shows/`) + - Manifest v2 stores active content paths; files automatically moved when prefixes change + - Root path support (`/`, `.`, or empty string all normalize to device root) + - Content path duplicate validation (no two content types can share the same prefix) + - Video scanning support for mass-storage devices (.m4v, .mp4, .mov, .avi, .mkv) + + **New in daemon (`@podkit/daemon`):** + - Mass-storage device polling via `PODKIT_MASS_STORAGE_PATHS` env var (colon/comma separated) + - Second `DevicePoller` + `SyncOrchestrator` pair for mass-storage devices + - No-op mount/eject runners (mass-storage devices are externally managed) + - Graceful shutdown handles both iPod and mass-storage sync pipelines + + **Configuration:** + + ```toml + [devices.echo] + type = "echo-mini" + path = "/Volumes/ECHO" + + # Optional capability overrides (mass-storage only) + artworkMaxResolution = 800 + supportedAudioCodecs = ["aac", "mp3", "flac"] + + # Optional content path overrides (mass-storage only) + musicDir = "/" # Place music at device root + moviesDir = "Films" # Custom movies directory + tvShowsDir = "TV Shows" # Custom TV shows directory + ``` + + **Environment variables for content paths:** + - `PODKIT_MUSIC_DIR` — global default music directory + - `PODKIT_MOVIES_DIR` — global default movies directory + - `PODKIT_TV_SHOWS_DIR` — global default TV shows directory + +- [`34ad4d3`](https://github.com/jvgomg/podkit/commit/34ad4d39104600f1363bd434518b10b3399a652c) Thanks [@jvgomg](https://github.com/jvgomg)! - Expose `pathTemplate` as a per-device config option for mass-storage devices, allowing user-customisable folder structures. + + Configurable via: + - `[devices.] pathTemplate = "..."` in TOML + - `PODKIT_PATH_TEMPLATE` env var (applied as a global default for mass-storage devices) + + Variables: `{albumArtist}`, `{artist}`, `{album}`, `{title}`, `{trackNumber}`, `{discNumber}`, `{totalDiscs}`, `{genre}`, `{year}`, `{ext}`. The template must contain `{title}` and `{ext}` and is rejected on iPod devices (iPod paths are managed by libgpod, not by template). + + Changing the template between syncs triggers the existing self-healing relocate flow — existing files are moved via `fs.rename()` to match the new layout, with no re-transcoding. Adds, removes (`--delete`), and template-driven relocates all compose in a single sync operation. + +- [`f5d0082`](https://github.com/jvgomg/podkit/commit/f5d00829f3b1a80453bdc4f7e6599566f7f02bb3) Thanks [@jvgomg](https://github.com/jvgomg)! - Reconcile USB-inquiry and block-device discovery so each connected iPod renders once in `podkit device scan`. Previously, `device scan` could surface the same physical iPod twice on Linux when both pipelines independently identified it. The orphan entry also surfaced a destructive remediation (`Needs partitioning — see: podkit device init`) on a healthy device. Both issues fixed: a new reconciliation primitive matches USB and block-device records by serial number (or disk identifier as fallback), and the readiness-failure copy now points at docs instead of suggesting an inappropriate command. + +- [`4ee5e2b`](https://github.com/jvgomg/podkit/commit/4ee5e2be470a93a54c2d54bc0aab257d7b92babe) Thanks [@jvgomg](https://github.com/jvgomg)! - Refuse HFS+ iPods on Linux at `device add`; warn at `device scan` + + iPods formatted as HFS+ are now refused on Linux at `podkit device add` time, with a clear message pointing at docs explaining how to reformat to FAT32. `podkit device scan` surfaces the same iPods with a `Filesystem not supported on Linux` warning instead of running readiness stages or suggesting destructive remediation. macOS HFS+ behaviour is unchanged. + + Why: the Linux kernel hfsplus driver refuses RW on journaled HFS+ (the iPod default), udev/blkid don't surface a filesystem UUID for HFS+ on Linux (breaking podkit's identity model), and udisksctl mount paths fall back to a generic name with no label. Each friction point has a partial fix; together they mean Linux + HFS+ is a second-class experience no matter how much we patch. Refusing cleanly with a docs link sharpens podkit's Linux story to "FAT32 iPods, supported well." + + Structured `--json` output preserves a stable error code (`UNSUPPORTED_FILESYSTEM_ON_LINUX`) so scripted callers can handle the refusal. + +- [`275c972`](https://github.com/jvgomg/podkit/commit/275c97295462547037e2c911c139654eb50d4af7) Thanks [@jvgomg](https://github.com/jvgomg)! - Sidecar artwork support and executor adapter fallback. + + The sync pipeline now picks up out-of-band artwork that lives alongside the audio file or on the source server, not just embedded pictures: + - **Directory adapter** detects peer `cover.jpg` / `folder.jpg` / `front.jpg` / `album.jpg` (also `.jpeg` / `.png`, case-insensitive) in the same directory as the audio file. When a file has no embedded picture, the sidecar bytes are used. Under `--check-artwork` the sidecar bytes are hashed and pinned in the sync tag, so swapping a `cover.jpg` for a new image is detected on the next sync. + - **Subsonic adapter** falls back to Navidrome's `getCoverArt` endpoint when the downloaded audio file body has no embedded picture. This closes the gap where a Navidrome library indexed sidecar art on the server but podkit silently dropped it on every sync. + - A one-time placeholder probe runs on every Subsonic `connect()` so Navidrome's static "no cover" image is filtered regardless of `--check-artwork`. + + Embed-in-the-file wins when both are present; the sidecar / API fallback only fires on a miss. Album-level caching means siblings on the same album share a single sidecar read or API request. + + Adapters gain an optional `getArtwork(item): Promise` method on `CollectionAdapter`. The executor calls it through the existing `AlbumArtworkCache` after embedded extraction returns null. + + **Known gap (deferred to TASK-370 / TASK-371 / TASK-372):** mass-storage devices accept the bytes via `setArtworkFromData`, which is a no-op for non-OGG/Opus containers — adapter-fallback bytes reach the device only when the output is OGG/Opus copy (via the existing taglib path) OR the target is an iPod (via the iTunesDB). For other mass-storage outputs the bytes are dropped silently today; the e2e artwork matrix fences those cells with a `[BUG] TASK-370` skip rather than failing the suite. + +- [`cac7fc1`](https://github.com/jvgomg/podkit/commit/cac7fc123861e97b10d31c83728a1e3f0431934e) Thanks [@jvgomg](https://github.com/jvgomg)! - Sidecar lifecycle cleanup + broader orphan-files surface (TASK-375). + + **Sync-time sidecar cleanup (mass-storage, sidecar-primary devices).** + `MassStorageAdapter.removeTrack` and `relocateTrack` now drop the + album's `cover.jpg` when the last managed audio file leaves the + directory. The delete is queued at the moment of removal and flushed + in a new `save()` stage that re-evaluates the predicate per entry, so + a re-add inside the same save cycle (whether through `writeSidecar` or + via a pipeline that skipped artwork on a hash match) cleanly cancels + the queued delete. The previous behaviour left a dangling `cover.jpg` + and a stale manifest entry forever; sync-time cleanup keeps the + invariant "every managed sidecar has at least one managed audio + sibling in its dir" alive. + + **Doctor's orphan-files check no longer filters by extension.** The + mass-storage walker previously surfaced only audio/video files as + orphan candidates. Any other file in your content directories + (sidecar images, lyrics `.lrc`, playlist `.m3u`, stray documents) was + silently dropped. Now the check considers any non-debris file in the + configured content roots — confirmation-gated repair stays unchanged + so you review the list before anything is deleted. This is the + backstop for sidecars on devices that synced before sync-time cleanup + existed, and surfaces unmanaged user-placed files in podkit's + territory you may want to clear out. + + **Migration:** on a rockbox device with a pre-existing user-placed + `cover.jpg` that podkit never wrote, the orphan check will now flag + it. The confirmation prompt is your safety; review before repairing. + After deletion, the next sync re-issues a managed sidecar. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`f72fa01`](https://github.com/jvgomg/podkit/commit/f72fa0170872fc0a6e5719b4509abae24e6414cd) Thanks [@jvgomg](https://github.com/jvgomg)! - Refactor sync engine to be fully content-type-agnostic with per-handler operation types. + + **Breaking:** `createMusicHandler()` and `createVideoHandler()` now take a config object at construction instead of using `setTransformsConfig()`/`setExecutionConfig()`. Removed `HandlerDiffOptions`, `HandlerPlanOptions`, `MusicExecutionConfig` types. Renamed `MusicExecutor` to `MusicPipeline`. Removed legacy planner functions (`createMusicPlan`, `planVideoSync` and related helpers). + + **New:** `MusicSyncConfig`, `VideoSyncConfig`, `MusicTrackClassifier`, `VideoTrackClassifier`, `MusicOperationFactory`, `MusicOperation`, `VideoOperation`, `BaseOperation` types. Handlers now own their operation types via `TOp` type parameter on `ContentTypeHandler`. + +- [`7bf7127`](https://github.com/jvgomg/podkit/commit/7bf7127d3141ce4b91138e3284b18aa5e8ea5984) Thanks [@jvgomg](https://github.com/jvgomg)! - Unify sync-engine error and warning handling (architecture sweep) + + Settles error and warning responsibilities across the sync engine. Hard + failures throw typed errors that carry their own category; soft signals + flow through an injected `WarningSink` and surface alongside hard errors + in `SyncOutput.warnings`. `console.warn` / `console.error` is now banned + in core. + + See `documents/architecture/sync/error-handling.md` for the full + responsibility model. + + ## Breaking API changes + + ### Types + - **`SyncWarning` and `ExecutionWarning` types removed.** Replaced by a + single `Warning` type with `phase: 'plan' | 'execute'`. Track + references are now structured (`WarningTrackRef = {artist, title, album?}`) + rather than a mix of `CollectionTrack[]` and an inline object. + + ```ts + // before + import type { SyncWarning, ExecutionWarning } from '@podkit/core'; + // after + import type { Warning, WarningPhase, WarningType, WarningTrackRef } from '@podkit/core'; + ``` + + - **`SyncPlan.warnings`** is now `Warning[]` (always `phase: 'plan'`). + - **`ExecuteResult.warnings`** is now `Warning[]` (always `phase: 'execute'`). + - **`CollectionAdapter.getPlanWarnings?()`** now returns `Warning[]`. + + ### New types + - **`Warning`, `WarningPhase`, `WarningType`, `WarningTrackRef`, + `WarningSink`** — the unified warning surface. + - **`CategorizedSyncError`** — abstract base class for all typed sync + errors. Subclasses declare `readonly category: ErrorCategory` so the + pipeline's categorizer reads it off the class instead of inspecting + the message body. + - **`DatabaseWriteError`** — wraps libgpod failures at the + `IpodAdapter` boundary so iTunesDB errors categorize as `database` + (no retry) rather than falling through to op-type fallback. + - **`PictureWriteError`**, **`MoveError`** — typed siblings of the + existing `TagWriteError` / `SidecarWriteError`, now also extending + `CategorizedSyncError`. + + ### `DeviceAdapter` contract + - New optional **`setWarningSink(sink: WarningSink): void`** method. + Adapters that emit execute-phase warnings (`IpodAdapter`, + `MassStorageAdapter`) must implement it. The pipeline injects its + accumulator sink at execute start. + + ## Breaking CLI JSON output changes + + The `sync` command's JSON output replaces the prior two warning fields + with a single unified array: + + ```diff + { + "success": true, + - "planWarnings": [{ "type": "lossy-to-lossy", "message": "...", "trackCount": 2, "tracks": [...] }], + - "executionWarnings": [{ "type": "artwork", "track": "Artist - Title", "message": "..." }] + + "warnings": [ + + { + + "phase": "plan", + + "type": "lossy-to-lossy", + + "message": "...", + + "trackCount": 2, + + "tracks": [{"artist": "...", "title": "...", "album": "..."}] + + }, + + { + + "phase": "execute", + + "type": "artwork", + + "message": "...", + + "trackCount": 1, + + "tracks": [{"artist": "...", "title": "...", "album": "..."}] + + } + + ] + } + ``` + + Filter by `warning.phase` to recover the prior split. Track refs + inside warnings are now structured objects rather than pre-formatted + strings — consumers can format them as they wish. + + ## Behaviour fixes + - **Execute-phase warnings now surface in `--json`.** The + `executionWarnings` field was declared on `SyncOutput` but never + populated by the CLI's real-run path — artwork extraction failures, + iPod portable tag-write misses, and mass-storage vanished-relocate + events were accumulated by the pipeline and silently dropped before + reaching JSON. They now appear in the unified `warnings` array. + - **`IpodAdapter` mutators (`addTrack`, `updateTrack`, `removeTrack`) + wrap libgpod failures in `DatabaseWriteError`.** Without the wrap, + libgpod errors during these mutators would categorize as `copy` via + the op-type fallback and retry once. iTunesDB failures now correctly + categorize as `database` (no retry). + - **Mass-storage picture-write stage normalized to collect-and-aggregate.** + Was `Promise.all` fail-fast with an untyped rejection; now + `runWithConcurrency` + settled-all + `PictureWriteError` + map-cleared- + before-throw, matching the tag-write stage convention. + - **CLI text-mode now prints an execute-phase warning summary** at the + end of a real sync run (grouped by warning type; expand with `-v`). + Previously these warnings were invisible to text-mode users. + + ## New text-mode CLI behaviour + + A new `Warnings:` block appears in the sync summary when execute-phase + warnings landed during the run: + + ``` + === Summary === + + Synced 152 items successfully + Duration: 8m 14s + + Warnings: 3 + artwork: 2 + tag-write: 1 + (re-run with -v for details) + ``` + +- [#59](https://github.com/jvgomg/podkit/pull/59) [`1ec30ac`](https://github.com/jvgomg/podkit/commit/1ec30acca1109178012db3913a60967a2087fb5b) Thanks [@jvgomg](https://github.com/jvgomg)! - Automated iPod device identification via SysInfoExtended. + + Modern iPods (post-2006) ship without a populated `SysInfo` file after iTunes restore. Without it, libgpod treats the device as generic — artwork breaks, ALAC support is unknown, and database checksums fail on Classic 6/7G and Nano 3G+. podkit now reads SysInfoExtended directly from iPod firmware over USB during `device add`, so first-time setup works with no manual tooling. + + **User-visible:** + - `podkit device add` identifies the exact model (e.g. "iPod nano 8GB Black (3rd Generation)") with no input + - `podkit doctor` detects missing SysInfoExtended and offers `--repair sysinfo-extended` + - `podkit sync` works correctly on first run with full capability detection + - Hash72 (Nano 5G) and HashAB (Nano 6G) devices get clear limitation messages + - SysInfoExtended write is gated on user confirmation during `device add` + + **Core (`@podkit/core`):** + - Unified iPod model registry — single table, both `0x120x`/`0x126x` USB ID ranges, 190+ serial-suffix → model mappings, checksum-type classification per generation + - `ensureSysInfoExtended()` orchestrator: check existing → USB read → validate XML → write + - USB discovery now exposes `serialNumber`, `busNumber`, `deviceAddress`; `resolveUsbDeviceFromPath()` on macOS + Linux + - Readiness pipeline: checksum-aware severity (hash58+ devices fail without SysInfoExtended; pre-checksum devices warn) + - `READINESS_RULES` declarative array replaces ad-hoc `determineLevel()` logic + - New `sysinfo-extended` diagnostic check + - Recognizes `P` / `F` model prefixes in SysInfo + + **libgpod-node (`@podkit/libgpod-node`):** + - `readSysInfoExtendedFromUsb()` N-API binding, resolved via `dlsym` at runtime so it loads gracefully on systems where libgpod lacks the symbol + - Prebuild patches upstream libgpod 0.8.3 to move `itdb_usb.c` from `tools/` into the library; libusb 1.0.27 built from source on all 6 platforms + - `--whole-archive` / `-force_load` linker flags preserve the dlsym symbol in the `.node` binary + + **CLI (`podkit`):** + - `device add` attempts SysInfoExtended read after mount, before DB init; enriches model name in summary + - `doctor` adds suggested-actions section, drops destructive sysinfo guidance + - `device scan` and `doctor` show clearer SysInfo readout + +- [`c5cba69`](https://github.com/jvgomg/podkit/commit/c5cba6998283663b42659f02b17b194ab256c137) Thanks [@jvgomg](https://github.com/jvgomg)! - Polish on the convergent-metadata work (TASK-327 follow-up): + - **Tag-write concurrency cap.** `save()` now caps in-flight tag writes at 16 via a small `runWithConcurrency` helper instead of firing every pending write at once. Avoids `EMFILE` on large libraries. + - **Aggregated tag-write errors.** Failure messages now begin with `tag write failed` so the executor's error categorizer classifies them as file-I/O (`copy`) rather than risking a path-keyword mis-classification. + - **WAV/AIFF on mass-storage.** Podkit transcodes WAV and AIFF source files to a managed codec before placing them on a mass-storage device, even when the device firmware can play them. RIFF/IFF tag-writing is unreliable. Presets continue to list these codecs for documentation. iPod is unaffected (libgpod / iTunesDB handle metadata for WAV/AIFF). + - **OGG Vorbis tag round-trip tests.** Now run on builds with libvorbis (skipped automatically when absent). + - **Shared TagFields helpers.** `buildTagFieldsFromInput` and `diffTagFields` replace three duplicate field-by-field walks across adapters. + - **`TransferMode` type unified.** Removed `'fast' | 'optimized' | 'portable'` duplication between `DeviceTrackInput`, `DeviceTrackMetadata`, and the canonical `TransferMode` in `transcode/types.ts`. Drops several inline type casts. + - **Docs.** `transferMode` now has a dedicated section in `docs/reference/config-file.md` explaining the iPod vs mass-storage contract and migration churn. `pathTemplate` (from the prior release) and `PODKIT_PATH_TEMPLATE` are now documented in the config reference and environment-variables reference. + +- [`1c3ebc3`](https://github.com/jvgomg/podkit/commit/1c3ebc381276accdb8361f50454b90c75f2391df) Thanks [@jvgomg](https://github.com/jvgomg)! - Add three-tier transfer mode system controlling how files are prepared for the device. + + **Transfer modes:** + - `fast` (default): optimizes for sync speed — direct-copies compatible files, strips artwork from transcodes + - `optimized`: strips embedded artwork from all file types (including MP3, M4A, ALAC copies) via FFmpeg stream-copy, reducing storage usage without re-encoding + - `portable`: preserves embedded artwork in all files for use outside the iPod ecosystem + + **Configuration:** + - `transferMode` config option (global and per-device) + - `--transfer-mode` CLI flag + - `PODKIT_TRANSFER_MODE` environment variable + + **Selective re-processing:** + - `--force-transfer-mode` flag re-processes only tracks whose transfer mode doesn't match the current setting + - `PODKIT_FORCE_TRANSFER_MODE` environment variable + - Works on all file types including direct copies (unlike `--force-transcode` which only affects transcoded tracks) + + **Device inspection:** + - `podkit device music` and `podkit device video` stats show transfer mode distribution + - Missing transfer field flagged alongside missing artwork hash in sync tag summary + - New `syncTagTransfer` field available in `--tracks --fields` for querying transfer mode data + - Dry-run output shows configured transfer mode + + **Under the hood:** + - Granular operation types: `add-direct-copy`, `add-optimized-copy`, `add-transcode` (and upgrade equivalents) + - Sync tags written to all tracks including direct copies (`quality=copy`) + - `DeviceCapabilities` abstraction for device-aware sync decisions + - Sync tag field `transfer=` tracks which mode was used per track + +- [`ec8dc85`](https://github.com/jvgomg/podkit/commit/ec8dc8549447b0178a8746b8cda2b8b7908b9d04) Thanks [@jvgomg](https://github.com/jvgomg)! - Unify the unsupported-device UX across `podkit device add`, `device scan`, `device info`, `sync`, and `doctor`. Every command now composes identity via the same cascade primitive (`resolveIpodModel(bag)`) — no command re-implements the check, no command leaks `libgpod` into user-facing copy. + + Key behaviour changes: + - `device add` on an unsupported device (hashAB nano, etc.) now asks "Add anyway? [y/N]" rather than hard-refusing. Confirmed devices are recorded with `unsupported: true` in config; `--yes` flips the default to accept. + - `device add` against an iOS device (iPod touch) now surfaces the canonical unsupported message instead of the generic "No iPod devices found". + - `device scan` headers show the resolved model name (e.g. "iPod touch 5th generation") instead of "Unknown iPod (USB only)". + - `sync --dry-run` refuses cleanly on unsupported devices with the canonical message — no track plan generated. + - `sync` on a supported device with SysInfoExtended present resolves identity via the cascade; the legacy "Could not identify iPod model" warning is gone for that case. + - `device info` renders the cascade `displayName` instead of the libgpod-derived `info.device.modelName`. + - `doctor` on an unsupported device suppresses repair suggestions that would mutate device state and surfaces the canonical unsupported message instead. + + Wording is centralised in `@podkit/core` (`makeUnsupportedReasonFromAssessment` / `makeUnsupportedReasonFromModel`) — every consumer imports. + +- [`e825ee1`](https://github.com/jvgomg/podkit/commit/e825ee1dd4933ecbfd070dda27f96f43056f0baf) Thanks [@jvgomg](https://github.com/jvgomg)! - Replace koffi-based libusb FFI with the `usb` npm package for USB firmware inquiry, eliminating the runtime libusb system dependency. + + The `@podkit/ipod-firmware` USB transport now uses the `usb` npm package, whose prebuilt N-API bindings statically link libusb. End-user binaries embed that prebuild via Bun `--compile`; no system `libusb-1.0` is required at runtime. + + Public-surface changes in `@podkit/ipod-firmware`: + - **Removed:** `loadLibusb`, `LibusbBinding`, `LibusbPtr`, `LibusbLoadResult`, `_resetLibusbCacheForTests`. The koffi-shaped binding interface is gone. + - **Added:** `loadUsb`, `UsbBinding`, `UsbDeviceHandle`, `UsbLoadResult`, `_resetUsbCacheForTests`. Higher-level `withOpenDevice(bus, devnum, fn)` seam — implementations handle enumeration, open, and cleanup internally. + - **Added:** `setLogger(fn | null)`, `FirmwareLogger`, `FirmwareLogEvent`. Library no longer writes to stderr/stdout; consumers install a receiver and decide format/destination. The CLI installs one when `-v` is passed. + - **Added:** `@podkit/ipod-firmware/bundle` subpath export with `bundleUsbNative(nativeModule)` for single-file binary builds. See `agents/ipod-firmware.md` for the staging recipe. + - **Renamed:** `UsbInquiryError.libusbCode` → `UsbInquiryError.libusbStatus`. The new field carries `LIBUSB_TRANSFER_*` status codes (positive enum) from the `usb` npm package, not the negative `LIBUSB_ERROR_*` codes the koffi path returned. + + Doctor's `inquiry-methods` check no longer reports libusb availability — the USB transport is bundled and always present in shipped binaries. The check now reports SCSI transport availability only, which remains user-actionable on Linux (udev permissions) and macOS (iPodDriver.kext). + +- [`72fd2c8`](https://github.com/jvgomg/podkit/commit/72fd2c8521cfa8f8d7102a6d1707d8282f67e6d2) Thanks [@jvgomg](https://github.com/jvgomg)! - Add user-defined mass-storage device presets via the new `[presets.]` config section. + + Declare a custom DAP in `~/.config/podkit/config.toml`: + + ```toml + [presets.my-walkman] + extends = "generic" + manufacturer = "Sony" + productName = "NW-A105" + supportedAudioCodecs = ["aac", "flac", "mp3"] + artworkMaxResolution = 240 + musicDir = "MUSIC" + ``` + + Then add a device that uses it: + + ```sh + podkit device add -d walkman --type my-walkman --path /Volumes/MyWalkman + ``` + + `--type` previously rejected any value that wasn't `ipod`, `echo-mini`, `rockbox`, or `generic`. It now also accepts any preset id declared in the config. Built-in ids remain authoritative; `[presets.echo-mini]` collisions are refused at load time. + + `device list`, `device info`, sync, and doctor mass-storage paths consult the merged registry so user-preset content-path and capability defaults flow through correctly. Two devices typed to the same user preset id resolve independently — they share the preset baseline but per-device overrides apply on top. + + When a user preset declares `wav` or `aiff` in `supportedAudioCodecs`, the loader emits the same warning that `[devices.X]` overrides already produce: podkit transcodes sources in those formats rather than direct-copying. + +### Patch Changes + +- [`bb2e637`](https://github.com/jvgomg/podkit/commit/bb2e6374151605d11baf052c452f10a842e5353e) Thanks [@jvgomg](https://github.com/jvgomg)! - Externalize `koffi` and `usb` from the published `bun build` bundles. Koffi loads its native binding via `eval('require')(filename)`; bun's bundler shims top-level `require` as `__require` (via `createRequire(import.meta.url)`) but does not inject `require` into eval'd literals, so the bundled CLI hit `ReferenceError: require is not defined` whenever the SCSI inquiry path was actually reached. The native loaders are now resolved at runtime via `node_modules`, which is also more correct for `usb` (whose `bun build`-time prebuild only matched the build host's platform). + + The standalone-binary path (`bun --compile` via `compile.sh`) is unchanged — it stages platform-specific `.node` files and uses static `require()` in `compile-entry.js`, which works correctly. A bug in `compile.sh`'s linux-arm64 branch is also fixed: the script previously constructed `linux-arm64/node.napi.${USB_VARIANT}.node` (where `USB_VARIANT` is `glibc` or `musl`) but the `usb` package only ships `linux-arm64/node.napi.armv8.node` — no glibc/musl split exists for arm64. The script now selects the armv8 prebuild unconditionally on arm64. + + `@podkit/ipod-firmware` is also externalized from the `@podkit/core` and `@podkit/devices-ipod` builds, so neither package's `dist/index.js` re-inlines firmware (and therefore koffi/usb imports). Bundle content-check tests under `packages/*/src/bundle.test.ts` assert that no `eval("require")` slips into any published bundle. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`0ef210b`](https://github.com/jvgomg/podkit/commit/0ef210be6e5fc38203e5501d33cc1bb978ecc0c6) Thanks [@jvgomg](https://github.com/jvgomg)! - Add `--clean-artists` / `--no-clean-artists` / `--clear-clean-artists` options to `podkit device set` + + The clean artists transform can now be toggled per-device from the CLI instead of requiring manual config file edits. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`56c7ec3`](https://github.com/jvgomg/podkit/commit/56c7ec36fb00b6996beffdce76eb17a23211c628) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix shell completions namespace conflict when multiple podkit binaries are installed. + + The `--cmd` flag now derives the completion function prefix from the binary name (`podkit-dev` → `_podkit_dev`), so `podkit` and `podkit-dev` each get an isolated namespace and their completion scripts no longer clobber each other. The `podkit-dev` binary built via `install:dev` now reports a `-dev` version suffix. + +- [`348f2c5`](https://github.com/jvgomg/podkit/commit/348f2c53cec06598903b5cf128663d5121c46865) Thanks [@jvgomg](https://github.com/jvgomg)! - Redesign `podkit device add` to be slick and informative. Previously, plugging in a post-2006 iPod (nano 2G, nano 7G, iPod 5G) and running `device add` displayed the device as `Model: Invalid` (libgpod's wording for an empty SysInfo file) and instructed the user to manually write a SysInfo file with `ModelNumStr: MA147` — neither friendly nor accurate. + + The new flow: + 1. **Identity is cascade-resolved** from USB product ID, classic SysInfo, SysInfoExtended, and serial — whichever sources are available. Display reads `Found iPod nano (2nd Generation):` rather than `Model: Invalid`. + 2. **A single combined prompt** asks `Add this iPod as "X" and write SysInfoExtended? [Y/n]` when SysInfoExtended is missing and USB is reachable. Confirming triggers firmware inquiry, writes SysInfoExtended, and persists to config in one step. + 3. **Capabilities are derived from the cascade-resolved generation**, not from libgpod's pessimistic fallback. Negative capabilities cite the reason (`- Video (not supported on iPod nano 4GB Green (2nd Generation))`). + 4. **The follow-up tip** suggests `podkit sync -d --dry-run`, not "go run two more commands". + + New flag `--no-firmware-inquiry` skips the firmware fetch+write when used with `--yes` — for the case where the user wants to defer the write or doesn't have the device connected over USB. + + Internal API changes in `@podkit/core`: + - **Added** `assessIpodIdentity(mountPoint, opts?)` returning `IpodIdentityAssessment` — pure cascade-driven assessment (no writes). Combines all available identification sources and returns `{ model, capabilities, firmwareInquiry: 'present' | 'missing' | 'unwritable', needsChecksum }`. The CLI now composes from this primitive instead of reaching into libgpod for identity. + + The misleading `device-validation.ts` warning text (`Ensure /Volumes/X/iPod_Control/Device/SysInfo exists with your model number (e.g., "ModelNumStr: MA147")`) has been replaced with a pointer to the canonical fix: `podkit doctor --repair sysinfo-extended`. + +- [`6db8fb0`](https://github.com/jvgomg/podkit/commit/6db8fb0c587cfd9e350516631b2030f165c542ce) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit device add` now refuses cleanly when an iPod's volume UUID can't be read, with a clear message + structured error code (`VOLUME_UUID_REQUIRED`). Previously a synthetic `manual-...` UUID could be persisted in config, which then broke replug detection and `podkit doctor -d ` lookups. Most-common cause (HFS+ on Linux) was already addressed in TASK-317.12; this is the defensive catch-all for any remaining edge cases (corrupt partition tables, unusual layouts). + +- [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6) Thanks [@jvgomg](https://github.com/jvgomg)! - P4 — device-capability architecture complete. All in-tree migration finished; deprecated shims removed. + + **Breaking changes in `@podkit/core`:** The following symbols have been removed from the public API: `createIpodCapabilities`, `LibgpodDeviceInfo`, `DEVICE_PRESETS`, `DevicePreset`, `getDevicePreset`, and `resolveDeviceCapabilities`. Callers must migrate before upgrading. + + Migration guide: + - `createIpodCapabilities(libgpodInfo)` → `resolveCapabilities(identity)` (preferred, identity-driven) or `resolveIpodModelCapabilities(modelFromLibgpodInfo(libgpodInfo))` for callers that genuinely hold libgpod data. + - `getDevicePreset(deviceType)` → `BUILT_IN_PRESETS[deviceType]` from `@podkit/devices-mass-storage`. + - `DEVICE_PRESETS` → `BUILT_IN_PRESETS` from `@podkit/devices-mass-storage`. + - `resolveDeviceCapabilities(type, overrides)` → `resolveCapabilities(identity, { overrides })` from `@podkit/core`. + - The `core/device/sysinfo-extended` shim path is gone; import `readSysInfoExtended`, `writeSysInfoExtended`, and `ensureSysInfoExtended` from `@podkit/ipod-firmware` directly. + + See ADR-295.07 for the full architectural rationale. + + **New in `@podkit/ipod-firmware`:** SysInfoExtended file I/O is now owned by this package: `readSysInfoExtended`, `writeSysInfoExtended`, `ensureSysInfoExtended`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR`. Diagnostic helpers `compareSysInfoConsistency` and `normaliseFireWireGuid` are also exported. `ParsedFirmware` gains the optional `modelNumber` field (populated from the `ModelNumStr` plist key when present). + + **New in `@podkit/device-types`:** `IpodModel`, `IpodChecksumType`, `IpodGenerationId`, `IpodGenerationIdLike`, `IpodModelSource`, and `IPOD_GENERATION_IDS` are now exported from this package (canonical home). `UsbConnectionInfo` has been removed — use `UsbFingerprint` instead. `IpodIdentity.notSupportedReason` is added for devices identified as iPods that podkit cannot fully support (e.g. iOS-mode devices). `DeviceCapabilities.artworkMaxResolution` is now `number | null` (null when the generation has no known limit or artwork is unsupported). + + **In `@podkit/devices-ipod`:** `IpodGeneration.supported` and `IpodGeneration.artworkMaxResolution: number | null` are new fields on every generation entry. `lookupByFamilyId` and `FAMILY_ID_TO_GENERATION` are now exported. `unsupported.ts` is populated with comprehensive Apple iOS device PIDs to allow early rejection of phones and tablets at the USB identification stage. + + No user-facing CLI behaviour changes. `podkit device scan`, `podkit device info`, and all sync paths behave identically to P3. + +- [`a78e5fe`](https://github.com/jvgomg/podkit/commit/a78e5fee4e47293c1935395bb157cb6574782625) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix three `podkit doctor` repair correctness bugs: + - `--repair sysinfo-consistency` now overwrites a stale on-disk SysInfoExtended (previously short-circuited on file existence, reporting success without rewriting). + - `--repair sysinfo-extended` no longer requires an existing iTunesDB — repairs without a `database` requirement skip the DB open so identity-populating repairs work on freshly formatted iPods. New `'database'` value on `RepairRequirement`. + - The readiness `SysInfoExtended:` status line distinguishes a missing file from a present-but-unparseable one. + +- [`785ad57`](https://github.com/jvgomg/podkit/commit/785ad57af6627059fe3a6d7e1fef475e82c34764) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit doctor` (artwork-rebuild repair) now runs a per-track source-file validity probe (stat + 16-byte magic-byte header check) before the album-cache lookup. Corrupt or unreadable source files always land in the `errors` bucket with a structured reason (`missing | unreadable | truncated | badMagic`) rather than inheriting a sibling track's cache success non-deterministically. + + The `details.errorDetails[*]` in doctor's JSON output now carries optional `path` and `reason` fields so users can act on specific bad files. Backward-compatible: existing `artist` / `title` / `error` fields are preserved. + + Magic-byte signatures cover FLAC, OGG/Opus, MP3 (ID3 and bare MPEG sync), MP4/M4A/AAC, WAV, AIFF/AIFC — matching the directory adapter's accepted extensions. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`3db3d88`](https://github.com/jvgomg/podkit/commit/3db3d887ae2cd19d01ba2c1f00b8682e783fac84) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix multiple bugs discovered during end-to-end Echo Mini hardware validation + + **Sync pipeline:** + - Create temp directory for optimized-copy operations (not just transcodes), fixing "No such file or directory" FFmpeg failures on mass-storage devices + - Capture last 1000 chars of FFmpeg stderr (instead of first 500) so actual errors aren't swallowed by the version banner + + **Device preset content paths:** + - Pass device preset content paths to adapter even when no user overrides exist, fixing Echo Mini's `musicDir: ''` being ignored and files landing in `Music/` instead of device root + + **Artwork:** + - Read embedded artwork during mass-storage device scan (`skipCovers: false`) so artwork presence is correctly detected, preventing false `artwork-added` upgrades on every sync + - Force `yuvj420p` (4:2:0) pixel format in artwork scale filter — JPEG with 4:4:4 chroma subsampling does not display on the Echo Mini + + **Sync tag and preset detection:** + - Treat `quality=copy` sync tags as in-sync when the classifier would also route the source as a copy, preventing false preset-upgrade detection on FLAC-capable mass-storage devices + - Route lossless sources to transcode (not copy) when quality preset is non-lossless, even if the device natively supports the source codec (e.g., FLAC device with quality=high should produce AAC) + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`7ebb7c5`](https://github.com/jvgomg/podkit/commit/7ebb7c5c0e1c7c3d549196347029d9ce660fcb8b) Thanks [@jvgomg](https://github.com/jvgomg)! - Use configurable device label in eject messages instead of hardcoded 'iPod' + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`1caab19`](https://github.com/jvgomg/podkit/commit/1caab1991d43739aaba3d9ae2e4a5dd6575f331a) Thanks [@jvgomg](https://github.com/jvgomg)! - Hard error on invalid `--fields` names with message listing valid fields; valid fields now listed in `--help` + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`3fe7853`](https://github.com/jvgomg/podkit/commit/3fe785330f8b92c21159ae253456942a92e7c8e2) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix stdout truncation when piping CLI output to another process. Commands that used `process.exit(1)` could terminate before stdout buffers flushed, truncating JSON output (e.g. `podkit init --json | node -e ...`). All error exit paths now use `process.exitCode = 1` and return normally, allowing Node.js to drain streams before exiting. + +- [`26733cc`](https://github.com/jvgomg/podkit/commit/26733cc77fd56681387b29e4241ad05e4d1fd348) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix blank source path in sync output for subsonic collections + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`e58ae80`](https://github.com/jvgomg/podkit/commit/e58ae806a494e3f526a828d4b72dab558ae4b121) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix config not found when running `podkit` under `sudo`. The default config path now resolves the invoking user's home directory via `SUDO_USER`/`DOAS_USER` and `/etc/passwd`, rather than using root's home. + +- [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix iPod model identification regressing to "Unknown iPod" after `doctor --repair sysinfo-extended` on pre-2006 devices (mini 2G), and tighten the package-boundary contract so consumers compose identity instead of injecting resolution policy. + + The bug: each consumer of `ensureSysInfoExtended` / `readSysInfoExtended` passed a serial-only `resolveModel` callback. When the 3-character serial suffix wasn't in `tables/serials.ts`, the resolver returned undefined and the device was displayed as "Unknown iPod" — even when a SysInfo file with a known `ModelNumStr` was sitting next to the SysInfoExtended on disk. + + The fix: + - **Removed** `ModelResolver` type and the `resolveModel` callback from `@podkit/ipod-firmware`. `readSysInfoExtended` and `ensureSysInfoExtended` now return a flat `SysInfoIdentity` bag (`firewireGuid?, serialNumber?, modelNumStr?, familyId?`). When a SysInfo file is on disk alongside SysInfoExtended, its `ModelNumStr` is read opportunistically. + - **Callers compose** with `resolveIpodModel(bag)` from `@podkit/devices-ipod`, which cascades modelNumStr → serial → productId → familyId → libgpodGeneration. The CLI no longer makes resolution decisions. + - **Added** `SYSINFO_PATH`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR` exported from `@podkit/ipod-firmware` and re-exported from `@podkit/core`. Consumers use these constants instead of duplicating the literal `iPod_Control/Device/...` paths. + - **Added** `S4G: '9804'` entry to `tables/serials.ts` (mini 2G 4GB Pink, sourced from real hardware, serial `JQ5141TFS4G`). + - **Post-write enrichment.** After `ensureSysInfoExtended` writes the file via USB inquiry, it now re-reads via `readSysInfoExtended` so the post-write identity bag includes `modelNumStr` from the SysInfo neighbour. Eliminates the cosmetic regression where the repair-success message showed a less-specific name than the subsequent `doctor` run. + +- [`eed4126`](https://github.com/jvgomg/podkit/commit/eed4126fe91ff64f00d74e8a2aaaae38ca6d786b) Thanks [@jvgomg](https://github.com/jvgomg)! - Improve the firmware-inquiry orchestrator's failure message so users can see what went wrong without `-vv`. The default error now names every transport attempted (USB, SCSI) with each one's failure reason on its own line and includes a remediation hint (e.g. `podkit doctor --repair udev-rule` for EACCES on `/dev/sg*` or `/dev/bus/usb/...`). The orchestrator also no longer short-circuits a planned SCSI fallback when USB hits a permission wall — both transports run if the plan calls for it. + +- [`151152a`](https://github.com/jvgomg/podkit/commit/151152ae835529730b3235a780550ec35ad685e2) Thanks [@jvgomg](https://github.com/jvgomg)! - macOS regression coverage for the m-18 TASK-317.\* hygiene cluster. Extends the test surface so the macOS-platform code paths that ship today (HFS+ supported, `system_profiler` `bsd_name` partition-suffix handling, `sysinfo-modelnum-mismatch` diagnostic framework, unsupported-cascade suppression, doctor section ordering and visibility, JSON envelope shape, TOML round-trip) all have explicit pinned assertions. + + Foundation: `DevicePersona` now carries an optional `platformDeviceInfoDarwin?: PlatformDeviceInfo[] | null` field, and `ipodMacosPlatformInfo(opts)` in `@podkit/device-testing/personas/builders` synthesises canonical macOS-shape records. Populated on `ipodMini2gPink` (FAT32) and `ipodNano4gHfsplus` (HFS+) as representative fixtures. + +- [`2a644af`](https://github.com/jvgomg/podkit/commit/2a644afa386dd091e8268c8db7dac906c48e44d8) Thanks [@jvgomg](https://github.com/jvgomg)! - Fixes an infinite loop in the music sync quality-upgrade path. When a track was upgraded via direct copy (no transcode) — for example when the source bitrate increased after a re-rip — `transferUpgradeToIpod` wrote the post-encode bitrate from the preparer, which is undefined for direct copies. The file was replaced on the device but the iPod-side bitrate stayed at the previous value, so the next sync detected the same upgrade again and re-fired it indefinitely. + + The fix resolves the post-upgrade bitrate as `prepared.bitrate ?? source.bitrate` so direct-copy upgrades carry the source bitrate through. The upgrade now converges on a single sync. + + Adds a `--force-sync-tags` bitrate backfill pass for pre-existing copied tracks whose iPod-side bitrate is 0 — symmetric with the existing artwork-hash baseline backfill. New users get correct bitrate tracking on first sync; existing users opt in with `--force-sync-tags`. + + Documents the upgrade-path semantics (format-upgrade gate, quality-upgrade gate, baseline write + backfill) in `documents/architecture/sync/upgrades.md`. + +- [`80fe65a`](https://github.com/jvgomg/podkit/commit/80fe65a022c65da512f571a8abf83f9385a649e6) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix SysInfoExtended SCSI-fallback on macOS for SCSI-only iPods (mini 2G, nano 2G, iPod 5G/5.5G). `device add` and `doctor --repair sysinfo-extended` now correctly fall back from USB → SCSI when the device does not respond to vendor control transfers, instead of failing with a misleading "Could not read device identity from USB" error. + + Internal API changes in `@podkit/ipod-firmware`: + - **Changed:** `ensureSysInfoExtended(mountPoint, fp, options?)` now takes a full `UsbFingerprint` instead of the previous `{ busNumber, deviceAddress }` shape. Required so the macOS SCSI transport can locate the IOService via vendorId/productId/serialNumber. `UsbDeviceAddress` is removed. + - **Added:** `inquireFirmwareDetailed(fp, opts?)` — like `inquireFirmware` but returns `{ firmware, plan, attempts }` so callers can distinguish which transports were attempted. `inquireFirmware` is unchanged for existing consumers. + - **Added:** `EnsureSysInfoExtendedOptions` type with `readFromUsb`, `resolveModel`, `inquireOptions` fields. Replaces the previous positional `(mountPoint, fp, readFromUsb, resolveModel)` signature. + + Internal API changes in `@podkit/core`: + - **Added:** `hasCompleteUsbFingerprint(fp): fp is CompleteUsbDevice` type guard exported from `@podkit/core`. + - **Added:** `CompleteUsbDevice` type — a `UsbFingerprint` with vendorId, productId, bus, devnum guaranteed present (serialNumber optional). + - **Changed:** `resolveUsbDeviceFromPath(path)` now also returns `vendorId` and `productId`. Linux extracts from sysfs `idVendor`/`idProduct`; macOS extracts from `system_profiler` JSON. + + User-facing error messages now differentiate between transport failures: "Could not read device identity from USB and SCSI" / "...from USB" / "...from SCSI" / "...no firmware inquiry transport is available on this system" / "...returned data but it could not be parsed". + +- [`63a69d1`](https://github.com/jvgomg/podkit/commit/63a69d11160770bcc5e251c7faf14d5c8887af13) Thanks [@jvgomg](https://github.com/jvgomg)! - New `podkit doctor` check `sysinfo-modelnum-mismatch` detects when the on-disk classic SysInfo file's `ModelNumStr` disagrees with the firmware-derived identity (e.g. SysInfo manually edited, or files copied from another iPod). Offers `--repair sysinfo-modelnum-mismatch` to overwrite the on-disk file with firmware-derived data. Identified during the TERAPOD (iPod 5G with iFlash mod) inventory pass — the SysInfo claimed `MA147` (5G) while the serial said `V9M`/`A446` (5.5G). + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`17eac11`](https://github.com/jvgomg/podkit/commit/17eac114719f93cef40beb58381e534a28ebc35f) Thanks [@jvgomg](https://github.com/jvgomg)! - Move spinners and progress bars to stderr and auto-suppress when stdout is not a TTY. Adds `--no-tty` flag for explicit suppression. Piped output (e.g. `podkit collection music --format json | jq .`) now produces clean stdout without needing `--quiet`. + +- [`cdebfb3`](https://github.com/jvgomg/podkit/commit/cdebfb3512f347356bc661722d2236b359776372) Thanks [@jvgomg](https://github.com/jvgomg)! - Extend the podkit udev rule to grant Apple-vendor USB device access (`/dev/bus/usb//`) in addition to the existing SCSI generic (`/dev/sg*`) coverage. Linux libusb-based firmware inquiry now works without sudo from SSH sessions, headless boxes, Docker containers, and CI — the SSH-session permission gap previously closed only the SCSI half. `podkit doctor --repair udev-rule` installs the extended rule and cleans up any legacy filename from previous installs. + +- [`f61a83b`](https://github.com/jvgomg/podkit/commit/f61a83b3a2d13612730f174759fd3b86edd42e82) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix `podkit device scan` reporting phantom "Unknown iPod (USB only)" entries for non-iPod USB peripherals (mice, hubs, Thunderbolt docks, Ethernet adapters, USB drives). Each phantom suggested `podkit device init` — a destructive operation that could mutate an unrelated device. + + Root cause was architectural: a single `discoverUsbIpods()` function mixed three concerns — USB enumeration, iPod-domain enrichment, and the function name's implied filter (which it didn't actually do). Refactored into clean layers: + - **`@podkit/core` — pure USB enumeration.** `enumerateUsb()` returns `EnumeratedUsbDevice[]` with vendor/product/serial/bus/devnum/diskIdentifier ONLY. No iPod-domain knowledge. + - **`@podkit/devices-ipod` — iPod classifier.** `classifyAsIpod(dev)` returns `IpodClassification | null` (matches Apple-vendor with iPod or iOS PIDs). + - **`@podkit/devices-mass-storage` — mass-storage classifier.** `classifyAsMassStorage(dev)` returns `MassStorageClassification | null` (matches `USB_PRESET_HINTS` entries like Echo Mini). + - **`@podkit/core` composer.** `classifyUsbDevices()` runs both classifiers and returns recognized devices as a tagged union; drops unknown peripherals. + - **CLI `device scan`** now calls `enumerateUsb()` → `classifyUsbDevices()` → renders by `kind`. No domain logic in the command layer. + + Added `kind: 'mass-storage'` rendering branch so mass-storage DAPs are no longer mis-labeled as "Unknown iPod". + + Removed `discoverUsbIpods` and the leaky `UsbDiscoveredDevice` type (which previously carried iPod-domain fields). Adding a new mass-storage device now means adding one entry to `USB_PRESET_HINTS` — no `@podkit/core` change required. Adding a new iPod generation means updating `@podkit/devices-ipod` tables — no other package changes. + + Also split `usb-discovery.ts` into `usb-enumeration.ts` (bus walk) + `usb-path-resolution.ts` (mount-path → fingerprint resolver) since those two concerns were unrelated. + +- [`6000868`](https://github.com/jvgomg/podkit/commit/6000868830d9437a6fff3c1a77adb254d9579fe7) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix `doctor --repair sysinfo-extended` showing unhelpful "Could not read device identity from USB" with no detail. The native USB binding now throws descriptive errors (e.g. "USB control transfer failed (bus 3, device 4)") instead of returning null silently. Also fix all doctor repair intro messages — they incorrectly said "Repairing X for N tracks" even for non-track operations like SysInfoExtended and orphan cleanup. Intro messages now use each repair's own description. + +- [`4598f8f`](https://github.com/jvgomg/podkit/commit/4598f8f3347cf40b94fdf1585215e5b0f54d9cf6) Thanks [@jvgomg](https://github.com/jvgomg)! - USB firmware inquiry consolidated into @podkit/ipod-firmware (P2 — m-18 device-capability architecture). + + **Breaking change in `@podkit/libgpod-node`:** The `readSysInfoExtendedFromUsb` function has been removed from the package's public exports. All in-tree callers were already routed through `@podkit/ipod-firmware` since P1 — only external consumers of `@podkit/libgpod-node` who called this function directly are affected. + + `@podkit/ipod-firmware` now owns the complete firmware inquiry surface: SCSI (Linux SG_IO + macOS IOKit) and USB (direct libusb-1.0 via koffi FFI). The P1 transitional shim that delegated USB reads to libgpod-node has been replaced by a native TypeScript implementation. No API change is visible to callers of `@podkit/ipod-firmware`. + + `@podkit/libgpod-node` no longer requires libusb at build or runtime. Distro packagers can now build the native binding without `libusb-1.0-0-dev` (Debian/Ubuntu), `libusb-devel` (Fedora/RHEL), or equivalent system packages. The `itdb_usb.c` patch, the `dlsym` shim, and the libusb pkg-config dependency have all been removed from the binding. + + No user-facing CLI behaviour changes. `podkit doctor` inquiry checks, `podkit device scan`, and all sync paths behave identically to P1. + +- Updated dependencies [[`0f3e4dd`](https://github.com/jvgomg/podkit/commit/0f3e4ddae134228b5e874b21db33f74547867b6c), [`036b107`](https://github.com/jvgomg/podkit/commit/036b1077748253385b6f4ff873a7cdb52c54b004), [`621b10a`](https://github.com/jvgomg/podkit/commit/621b10abbec3a8e369da9620733210fef4b76f99), [`89ff40c`](https://github.com/jvgomg/podkit/commit/89ff40c2adedd9fec38ae5ad0eb89b75525642f2), [`c5c0236`](https://github.com/jvgomg/podkit/commit/c5c0236c232cc3fa086fd3937b0e2fbe0f326185), [`bb2e637`](https://github.com/jvgomg/podkit/commit/bb2e6374151605d11baf052c452f10a842e5353e), [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af), [`513173d`](https://github.com/jvgomg/podkit/commit/513173d1832bf9ca2894214e97d9d65cf02c52a5), [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b), [`0cc39d3`](https://github.com/jvgomg/podkit/commit/0cc39d3c62343591127d5c79deed2478f8dc4f60), [`22dddf4`](https://github.com/jvgomg/podkit/commit/22dddf4803f4cfd7b004d80dffd83878a68b10f2), [`484fb0e`](https://github.com/jvgomg/podkit/commit/484fb0ea63eea297f19217d1acb96163a6754b05), [`348f2c5`](https://github.com/jvgomg/podkit/commit/348f2c53cec06598903b5cf128663d5121c46865), [`7534c2f`](https://github.com/jvgomg/podkit/commit/7534c2f19d81087413af8abbf764fe20cef61384), [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6), [`6747667`](https://github.com/jvgomg/podkit/commit/6747667049cd793fdb13e3d1bc1092651f8e969c), [`8bc3126`](https://github.com/jvgomg/podkit/commit/8bc3126ec415aa836b746ec921b6738abdd9e538), [`87cb87a`](https://github.com/jvgomg/podkit/commit/87cb87aef59ad366b4c6c2b4c22f897f0b84a54a), [`01ecedd`](https://github.com/jvgomg/podkit/commit/01ecedde623ff99e94c5cbda75ff9f9c9ecef632), [`667d66b`](https://github.com/jvgomg/podkit/commit/667d66b90e0979aaff381968358f2cfc78c8e581), [`03f1046`](https://github.com/jvgomg/podkit/commit/03f1046b70898b0282d0c96927bca60ee0d55eeb), [`78b0c71`](https://github.com/jvgomg/podkit/commit/78b0c71b9866306aecbb96f2a0e372a86564f2fc), [`14d83e5`](https://github.com/jvgomg/podkit/commit/14d83e5e59eb0a8a801850de775f9fdb4c0e7aa9), [`a78e5fe`](https://github.com/jvgomg/podkit/commit/a78e5fee4e47293c1935395bb157cb6574782625), [`e0f65f4`](https://github.com/jvgomg/podkit/commit/e0f65f4b0cf4fce28138849b7a85f2c3a7c1a613), [`785ad57`](https://github.com/jvgomg/podkit/commit/785ad57af6627059fe3a6d7e1fef475e82c34764), [`4efa15c`](https://github.com/jvgomg/podkit/commit/4efa15c7e42874e9dd88ef2731230d5314d83f20), [`3db3d88`](https://github.com/jvgomg/podkit/commit/3db3d887ae2cd19d01ba2c1f00b8682e783fac84), [`7ebb7c5`](https://github.com/jvgomg/podkit/commit/7ebb7c5c0e1c7c3d549196347029d9ce660fcb8b), [`34e8bf2`](https://github.com/jvgomg/podkit/commit/34e8bf2341111df1e8f85361b8047eed9f31665a), [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce), [`eed4126`](https://github.com/jvgomg/podkit/commit/eed4126fe91ff64f00d74e8a2aaaae38ca6d786b), [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4), [`09c4acd`](https://github.com/jvgomg/podkit/commit/09c4acdec349f200a649b2db15fe05345e380a7b), [`22dddf4`](https://github.com/jvgomg/podkit/commit/22dddf4803f4cfd7b004d80dffd83878a68b10f2), [`151152a`](https://github.com/jvgomg/podkit/commit/151152ae835529730b3235a780550ec35ad685e2), [`94c85d2`](https://github.com/jvgomg/podkit/commit/94c85d2a9d6c85875432a0ebecab540a9ebd67d7), [`efa14c6`](https://github.com/jvgomg/podkit/commit/efa14c623e7bda81066bd77142cddb28e4de615d), [`208e482`](https://github.com/jvgomg/podkit/commit/208e482db9730064a25e53e03121bdcfcbea6341), [`2a644af`](https://github.com/jvgomg/podkit/commit/2a644afa386dd091e8268c8db7dac906c48e44d8), [`f5d0082`](https://github.com/jvgomg/podkit/commit/f5d00829f3b1a80453bdc4f7e6599566f7f02bb3), [`4ee5e2b`](https://github.com/jvgomg/podkit/commit/4ee5e2be470a93a54c2d54bc0aab257d7b92babe), [`303c35a`](https://github.com/jvgomg/podkit/commit/303c35aea57c0f35f64481e12e5cb9298e9a5631), [`bb96778`](https://github.com/jvgomg/podkit/commit/bb96778dde9063267188b2b83535ec279cd5c550), [`7517a24`](https://github.com/jvgomg/podkit/commit/7517a2444abf629f8e032faf29c938eb74b9b51b), [`275c972`](https://github.com/jvgomg/podkit/commit/275c97295462547037e2c911c139654eb50d4af7), [`cac7fc1`](https://github.com/jvgomg/podkit/commit/cac7fc123861e97b10d31c83728a1e3f0431934e), [`f72fa01`](https://github.com/jvgomg/podkit/commit/f72fa0170872fc0a6e5719b4509abae24e6414cd), [`7bf7127`](https://github.com/jvgomg/podkit/commit/7bf7127d3141ce4b91138e3284b18aa5e8ea5984), [`c9c268e`](https://github.com/jvgomg/podkit/commit/c9c268ea4b25b39543e5c53a1928e72b4c31e0c8), [`1ec30ac`](https://github.com/jvgomg/podkit/commit/1ec30acca1109178012db3913a60967a2087fb5b), [`80fe65a`](https://github.com/jvgomg/podkit/commit/80fe65a022c65da512f571a8abf83f9385a649e6), [`63a69d1`](https://github.com/jvgomg/podkit/commit/63a69d11160770bcc5e251c7faf14d5c8887af13), [`52894c1`](https://github.com/jvgomg/podkit/commit/52894c1977bccd51a86929debfbaa7028a19dd61), [`c5cba69`](https://github.com/jvgomg/podkit/commit/c5cba6998283663b42659f02b17b194ab256c137), [`1c3ebc3`](https://github.com/jvgomg/podkit/commit/1c3ebc381276accdb8361f50454b90c75f2391df), [`cdebfb3`](https://github.com/jvgomg/podkit/commit/cdebfb3512f347356bc661722d2236b359776372), [`ec8dc85`](https://github.com/jvgomg/podkit/commit/ec8dc8549447b0178a8746b8cda2b8b7908b9d04), [`f61a83b`](https://github.com/jvgomg/podkit/commit/f61a83b3a2d13612730f174759fd3b86edd42e82), [`6000868`](https://github.com/jvgomg/podkit/commit/6000868830d9437a6fff3c1a77adb254d9579fe7), [`4598f8f`](https://github.com/jvgomg/podkit/commit/4598f8f3347cf40b94fdf1585215e5b0f54d9cf6), [`e825ee1`](https://github.com/jvgomg/podkit/commit/e825ee1dd4933ecbfd070dda27f96f43056f0baf)]: + - @podkit/core@0.7.0 + - @podkit/ipod-firmware@0.1.0 + - @podkit/device-types@0.1.0 + - @podkit/devices-mass-storage@0.1.0 + - @podkit/devices-ipod@0.1.0 + - @podkit/libgpod-node@0.2.0 + ## 0.6.0 ### Minor Changes diff --git a/packages/podkit-cli/package.json b/packages/podkit-cli/package.json index 54a75b41..adb908bd 100644 --- a/packages/podkit-cli/package.json +++ b/packages/podkit-cli/package.json @@ -1,6 +1,6 @@ { "name": "podkit", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "bin": { "podkit": "./dist/main.js" diff --git a/packages/podkit-core/CHANGELOG.md b/packages/podkit-core/CHANGELOG.md index db46ed1c..74ea4b1c 100644 --- a/packages/podkit-core/CHANGELOG.md +++ b/packages/podkit-core/CHANGELOG.md @@ -1,5 +1,764 @@ # @podkit/core +## 0.7.0 + +### Minor Changes + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`0f3e4dd`](https://github.com/jvgomg/podkit/commit/0f3e4ddae134228b5e874b21db33f74547867b6c) Thanks [@jvgomg](https://github.com/jvgomg)! - Add capability-gated clean artists transform + + Devices now declare whether they use Album Artist for browse navigation via `supportsAlbumArtistBrowsing`. When enabled globally, the `cleanArtists` transform is automatically suppressed on devices that support Album Artist browsing (Rockbox, Echo Mini, generic) and auto-applied on devices that don't (iPod). Per-device overrides still take priority. + + The dry-run summary shows when the transform is skipped (`Clean artists: skipped (device supports Album Artist browsing)`), and warns when it's force-enabled on a capable device. Both `sync --dry-run` and `device info` surface these in text and JSON output. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`036b107`](https://github.com/jvgomg/podkit/commit/036b1077748253385b6f4ff873a7cdb52c54b004) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix mass-storage directory structure to use album artist instead of track artist, and add template-based path system with self-healing relocate. + + **Bug fix:** Mass-storage devices (Echo Mini, Rockbox) now use `albumArtist` for directory grouping, falling back to `artist` when absent. Previously, compilation/various-artist albums had their tracks scattered across separate artist directories instead of being grouped together under the album artist. + + **Path templates:** File paths are now generated from a configurable template string (`{albumArtist}/{album}/{trackNumber} - {title}{ext}` by default). This lays the groundwork for user-customisable folder structures in a future release. + + **Self-healing relocate:** When source metadata changes (e.g. album artist corrected) or the path template changes, the next sync detects the path mismatch and moves files to their correct location via `fs.rename()` — no re-copying of audio data. Relocate operations appear in dry-run output and are tracked as a new `relocate` operation type. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`89ff40c`](https://github.com/jvgomg/podkit/commit/89ff40c2adedd9fec38ae5ad0eb89b75525642f2) Thanks [@jvgomg](https://github.com/jvgomg)! - Add audioNormalization device capability for device-appropriate Sound Check / ReplayGain handling + + Devices now declare their normalization support: 'soundcheck' (iPod), 'replaygain' (Rockbox), or 'none' (Echo Mini, generic). Devices with no normalization support skip soundcheck upgrade detection entirely, and the dry-run output hides or relabels the normalization line accordingly. Configurable via `audioNormalization` in device config. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`c5c0236`](https://github.com/jvgomg/podkit/commit/c5c0236c232cc3fa086fd3937b0e2fbe0f326185) Thanks [@jvgomg](https://github.com/jvgomg)! - Refactor audio normalization from iPod-centric Sound Check to a generic `AudioNormalization` type, and add ReplayGain album gain/peak support + + **Normalization refactoring:** + - Introduce `AudioNormalization` type that preserves source format fidelity (ReplayGain dB, iTunNORM soundcheck integers) without unnecessary round-trip conversions + - Replace scattered `soundcheck`, `soundcheckSource`, `replayGainTrackGain`, `replayGainTrackPeak` fields on `CollectionTrack` with a single `normalization` field + - Replace `soundcheck`, `replayGainTrackGain`, `replayGainTrackPeak` fields on `DeviceTrackInput` with `normalization` + - Conversions now happen at device boundaries: iPod adapter reads soundcheck integers, mass-storage adapter reads dB values directly + - Upgrade detection compares in dB space with 0.1 dB epsilon tolerance, eliminating false positives from integer rounding + - Metadata update diffs show human-readable dB values (e.g., `normalization: -7.5 dB → -6.2 dB`) instead of opaque integers + + **Album gain/peak support (TASK-253):** + - Extract `albumGain` and `albumPeak` from local file metadata and Subsonic API + - Write `REPLAYGAIN_ALBUM_GAIN` and `REPLAYGAIN_ALBUM_PEAK` via FFmpeg metadata flags during transcode + - Write album gain/peak via node-taglib-sharp tag writer for M4A files + - Thread album data through the full sync pipeline for mass-storage devices (Rockbox, etc.) + + **Breaking changes:** + - `CollectionTrack` shape: four normalization fields replaced by single `normalization?: AudioNormalization` + - `SoundCheckSource` type removed, replaced by `NormalizationSource` + - Upgrade reason `'soundcheck-update'` renamed to `'normalization-update'` in JSON output + - `soundCheckTracks` stat renamed to `normalizedTracks` + +- [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af) Thanks [@jvgomg](https://github.com/jvgomg)! - Disambiguate codec from container: `AudioCodec` value `'ogg'` is renamed to `'vorbis'` + + The `AudioCodec` slot previously used `'ogg'` to mean "OGG Vorbis." That conflated the OGG container with the Vorbis stream codec and could not represent Vorbis-in-OGG vs Opus-in-OGG as distinct device capabilities — Echo Mini (which plays Vorbis but hides `.opus` files) could not be modelled accurately. The codec slot now names the audio stream codec; `'vorbis'` replaces `'ogg'` in device presets and config. + + Configs containing `supportedAudioCodecs = ["…", "ogg", "…"]` under `[devices.*]` are migrated automatically by `podkit migrate` (config version 1 → 2). The migration is purely a string substitution inside the `supportedAudioCodecs` array; comments and surrounding formatting are preserved. + + Also lands as type-level groundwork for the future container-aware sync work: `AudioContainer`, `AUDIO_CONTAINERS`, `CODEC_CANONICAL_CONTAINER`, and an optional `DeviceCapabilities.containerConstraints` field. These are declared and exported but not yet read by the planner; they are placeholders for the upcoming Phase 2 work documented in the container-aware sync PRD. + + `DirectoryAdapter` now uses each `.ogg` file's probed stream codec (already populated by `music-metadata`) to distinguish Vorbis, Opus, and OGG-FLAC — same pattern as the existing AAC/ALAC distinction for `.m4a`. `SubsonicAdapter` additionally checks the API's `contentType` field for Opus-in-`.ogg`. The Subsonic check is best-effort because most Subsonic servers report container MIME (`audio/ogg`) regardless of stream codec; deeper probing is deferred until evidence of real-world impact. + + User-facing reference page added at `docs/reference/codec-support.md` explaining the codec/container model and what each `AudioCodec` value means. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`513173d`](https://github.com/jvgomg/podkit/commit/513173d1832bf9ca2894214e97d9d65cf02c52a5) Thanks [@jvgomg](https://github.com/jvgomg)! - Add configurable codec preference system for multi-device audio format support + + Users can now configure an ordered list of preferred audio codecs globally and per-device. The system walks the list top-to-bottom, selecting the first codec that is both supported by the target device and has an available FFmpeg encoder. This replaces the hardcoded AAC-only transcoding pipeline. + - **Default lossy stack:** opus → aac → mp3 (Rockbox devices get Opus automatically, iPods fall through to AAC) + - **Default lossless stack:** source → flac → alac (lossless files are kept in their original format when possible) + - **Quality presets are codec-aware:** "high" delivers perceptually equivalent quality regardless of codec (e.g., Opus 160 kbps ≈ AAC 256 kbps) + - **Codec change detection:** changing your codec preference re-transcodes affected tracks on the next sync + - **`podkit device info`** shows your codec preference list with supported/unsupported codecs marked + - **`podkit sync --dry-run`** shows which codec will be used and any codec changes + - **`podkit doctor`** warns when FFmpeg is missing an encoder for a preferred codec + + Configure via `config.toml`: + + ```toml + [codec] + lossy = ["opus", "aac", "mp3"] + lossless = ["source", "flac", "alac"] + + [devices.myipod.codec] + lossy = "aac" + ``` + + No configuration is required — existing setups work unchanged with sensible defaults. + +- [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b) Thanks [@jvgomg](https://github.com/jvgomg)! - Consolidate the two ways podkit expressed "this device is unsupported" into one canonical shape. `ReadinessUnsupportedReason` moves to `@podkit/device-types` (its natural home), and `resolveIpodModel(bag)` now returns it directly on `IpodModel.unsupportedReason` instead of the bare-string `notSupportedReason`. The bridge functions in `@podkit/core` (`makeUnsupportedReasonFromModel`, `makeUnsupportedReasonFromAssessment`) are removed — consumers read `model.unsupportedReason` directly. Internal refactor; user-facing CLI behaviour is unchanged. + +- [`0cc39d3`](https://github.com/jvgomg/podkit/commit/0cc39d3c62343591127d5c79deed2478f8dc4f60) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix track metadata convergence on mass-storage devices and add transfer-mode-aware on-disk tag writes for iPod portable. + + **Bug fix (mass-storage)**: `MassStorageAdapter.updateTrack` previously only wrote `comment`, OGG/Opus artwork, and ReplayGain to disk. All other metadata fields (title, artist, album, albumArtist, genre, year, trackNumber, discNumber, compilation) updated in-memory only — the file's embedded tags on the device were never rewritten. After a relocate or metadata-correction sync the next sync re-detected the same diff every time, looping forever as a zero-byte `update-metadata` op. + + `MassStorageAdapter` now queues every changed textual tag in a single `pendingTagWrites` map and flushes them as one `writeTags(filePath, fields)` call per file via `Promise.allSettled`. Per-file failures are aggregated and re-thrown so the sync executor can categorise them. + + **New behaviour (iPod portable)**: `IpodDeviceAdapter` now mirrors iTunesDB metadata into the on-disk file tags when `transferMode === 'portable'`. This makes files pulled off the iPod self-describing for re-import into a music library. `fast` and `optimized` modes still touch iTunesDB only — the iPod firmware reads metadata from iTunesDB and never falls back to file tags during playback, so paying the tag-rewrite cost in those modes would be wasted work. + + Tag writes are best-effort on iPod portable: failures are surfaced as warnings, not hard errors, because the iTunesDB write (the authoritative store for playback) already succeeded. + + **`addTrack` consistency**: When `transferMode === 'portable'`, both backends now also rewrite tags on first transfer to honour any collection-adapter transforms (e.g. clean-artists, Subsonic-side corrections) that FFmpeg's `-map_metadata 0` would otherwise copy through from the source. + + **On first sync after upgrade**: Existing mass-storage tracks will likely report a `metadata-correction` op on the next sync as stale on-disk tags converge to source values. These are zero-byte writes — no transcoding or transfers happen — but the operation list will look longer than usual for one cycle. + + **Scope notes**: + - Match-key changes (title, artist, album corrections) still produce a remove+add rather than a metadata update. By design: when those fields change, podkit treats it as a different track. + - Virtual-iPod (m-17) inherits the iPod behaviour automatically; no changes needed there. + +- [`22dddf4`](https://github.com/jvgomg/podkit/commit/22dddf4803f4cfd7b004d80dffd83878a68b10f2) Thanks [@jvgomg](https://github.com/jvgomg)! - Add capability adapter and fix iPod generation metadata + - Add `createIpodCapabilities()` adapter — maps libgpod device data to core `DeviceCapabilities`, using libgpod as authority for video/artwork support and supplementing codec support from generation metadata + - Add `toLibgpodGeneration()` mapping from detection-layer IDs (`nano_4g`) to libgpod IDs (`nano_4`) + - Fix artwork resolution: nano 3G→320, nano 4-6G→240, photo→320 (were all incorrectly 176) + - Fix ALAC support: add to 4th gen, Photo, Mini 2G, Touch 1-4, iPhone 1-4, iPad 1 + - Add video profiles for Touch, iPhone, and iPad generations (were missing, preventing video sync) + +- [`484fb0e`](https://github.com/jvgomg/podkit/commit/484fb0ea63eea297f19217d1acb96163a6754b05) Thanks [@jvgomg](https://github.com/jvgomg)! - Cross-process sync coordination: per-device lock, transcode owner-liveness, phantom auto-prune. + + **Per-device sync lock.** `podkit sync` now acquires a per-device PID-file lock at `.podkit/sync.lock` (mass-storage) or `iPod_Control/.podkit-sync.lock` (iPod) immediately after opening the device. A second concurrent `podkit sync` against the same device exits with the new `LOCK_HELD` code (exit code **4**) and a message naming the holding PID: + + ``` + Error: Another podkit process is already syncing /Volumes/TERAPOD (pid 12345). Wait for it to finish or kill it. + ``` + + Crash-safe: the lock file is unlinked in `finally`; if a process is SIGKILLed mid-sync, the next attempt detects the dead PID via liveness probe and takes over cleanly. `podkit sync --dry-run` does **not** take the lock (read-only by design). The daemon (`@podkit/daemon`) detects `LOCK_HELD` from the CLI subprocess and skips that cycle (no retry-spin). Read commands (`device scan`, `device info`, `device music`) are unaffected — writes only. + + **Transcode-tmp owner-liveness.** Each `podkit-transcode-/` scratch directory now writes a small `.owner` file at creation. The pre-sync sweep walker reaps dirs whose owner is dead or whose `.owner` is missing; live owners are left alone. This replaces the previous session-start-time floor, which missed the most common interruption case for the long-running daemon: its own prior cycle. + + **Phantom manifest auto-prune.** When the pre-sync sweep detects manifest rows whose backing audio file has vanished (mass-storage devices), it now prunes them atomically as part of the sweep. Previously the sweep emitted an advisory warning recommending `podkit doctor --repair orphan-files`; the advisory now fires only if the auto-prune itself fails. `doctor --repair orphan-files` remains as a backstop and is unchanged. + + **Filesystem support.** The shared liveness primitive uses only `O_CREAT|O_EXCL`, `unlink`, `rename`, `readFile`, `writeFile`, and `process.kill(pid, 0)` — stable across exFAT, FAT32, HFS+, APFS, ext4, and NTFS. We deliberately avoided `flock(2)` because its semantics on FAT-family filesystems (common for iPods) are platform-dependent. + + **Internal API additions** (`@podkit/core`): `acquireLock`, `LockHandle`, `LockHeldError`, `LockContestedError`, `getOwnIdentity`, `readOwnership`, `writeOwnership`, `isAlive`, `PidFileEntry` — all from a new `lib/pid-file.ts` primitive. `DeviceAdapter` gains an optional `prunePhantomManifest?(paths)` method (implemented on mass-storage, intentionally omitted on iPod). + + **Removed internals:** `SESSION_START_MS` constant and `sessionStartMsOverride` plumbing in `pre-sync-sweep.ts` are gone — replaced wholesale by the PID-file liveness probe. + + See `documents/architecture/sync/planning.md` §6 for the cross-process coordination design. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`7534c2f`](https://github.com/jvgomg/podkit/commit/7534c2f19d81087413af8abbf764fe20cef61384) Thanks [@jvgomg](https://github.com/jvgomg)! - Add device-aware diagnostics framework to `podkit doctor`. The doctor command now handles mass-storage devices gracefully instead of crashing when pointed at a non-iPod device. Diagnostic checks declare which device types they apply to, and the runner filters them automatically. JSON output now includes a `deviceType` field. + +- [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6) Thanks [@jvgomg](https://github.com/jvgomg)! - P4 — device-capability architecture complete. All in-tree migration finished; deprecated shims removed. + + **Breaking changes in `@podkit/core`:** The following symbols have been removed from the public API: `createIpodCapabilities`, `LibgpodDeviceInfo`, `DEVICE_PRESETS`, `DevicePreset`, `getDevicePreset`, and `resolveDeviceCapabilities`. Callers must migrate before upgrading. + + Migration guide: + - `createIpodCapabilities(libgpodInfo)` → `resolveCapabilities(identity)` (preferred, identity-driven) or `resolveIpodModelCapabilities(modelFromLibgpodInfo(libgpodInfo))` for callers that genuinely hold libgpod data. + - `getDevicePreset(deviceType)` → `BUILT_IN_PRESETS[deviceType]` from `@podkit/devices-mass-storage`. + - `DEVICE_PRESETS` → `BUILT_IN_PRESETS` from `@podkit/devices-mass-storage`. + - `resolveDeviceCapabilities(type, overrides)` → `resolveCapabilities(identity, { overrides })` from `@podkit/core`. + - The `core/device/sysinfo-extended` shim path is gone; import `readSysInfoExtended`, `writeSysInfoExtended`, and `ensureSysInfoExtended` from `@podkit/ipod-firmware` directly. + + See ADR-295.07 for the full architectural rationale. + + **New in `@podkit/ipod-firmware`:** SysInfoExtended file I/O is now owned by this package: `readSysInfoExtended`, `writeSysInfoExtended`, `ensureSysInfoExtended`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR`. Diagnostic helpers `compareSysInfoConsistency` and `normaliseFireWireGuid` are also exported. `ParsedFirmware` gains the optional `modelNumber` field (populated from the `ModelNumStr` plist key when present). + + **New in `@podkit/device-types`:** `IpodModel`, `IpodChecksumType`, `IpodGenerationId`, `IpodGenerationIdLike`, `IpodModelSource`, and `IPOD_GENERATION_IDS` are now exported from this package (canonical home). `UsbConnectionInfo` has been removed — use `UsbFingerprint` instead. `IpodIdentity.notSupportedReason` is added for devices identified as iPods that podkit cannot fully support (e.g. iOS-mode devices). `DeviceCapabilities.artworkMaxResolution` is now `number | null` (null when the generation has no known limit or artwork is unsupported). + + **In `@podkit/devices-ipod`:** `IpodGeneration.supported` and `IpodGeneration.artworkMaxResolution: number | null` are new fields on every generation entry. `lookupByFamilyId` and `FAMILY_ID_TO_GENERATION` are now exported. `unsupported.ts` is populated with comprehensive Apple iOS device PIDs to allow early rejection of phones and tablets at the USB identification stage. + + No user-facing CLI behaviour changes. `podkit device scan`, `podkit device info`, and all sync paths behave identically to P3. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`8bc3126`](https://github.com/jvgomg/podkit/commit/8bc3126ec415aa836b746ec921b6738abdd9e538) Thanks [@jvgomg](https://github.com/jvgomg)! - Add device readiness diagnostic system + - New 6-stage readiness pipeline (USB → Partition → Filesystem → Mount → SysInfo → Database) that checks every stage of device health + - OS error code interpreter translates errno values into actionable explanations + - USB discovery finds iPods even without disk representation (unpartitioned/uninitialized devices) + - Enhanced SysInfo validation detects missing, corrupt, or unrecognized model files + - Diagnostics framework now handles missing database gracefully (checks skip instead of crashing) + +- [`87cb87a`](https://github.com/jvgomg/podkit/commit/87cb87aef59ad366b4c6c2b4c22f897f0b84a54a) Thanks [@jvgomg](https://github.com/jvgomg)! - Move artwork operations from `DeviceTrack` onto `DeviceAdapter` (Option Z) + + Public API: the `DeviceTrack` and `IpodTrack` interfaces no longer expose + `setArtwork(path)`, `setArtworkFromData(bytes)`, or `removeArtwork()`. The + `DeviceAdapter` interface gains: + - `setTrackArtwork(track, imageData): Promise` — write artwork bytes; + adapter dispatches internally on `track.artworkSink` (iPod ArtworkDB, + mass-storage embedded tag, sidecar `cover.jpg`, or no-op). + - `removeTrackArtwork(track): Promise` (was `T`, optional) — clear + artwork. Now required on every adapter; iPod clears the ArtworkDB entry, + mass-storage is a deliberate no-op. + + The pipeline's `transferArtwork` is now a single `adapter.setTrackArtwork` + call instead of a three-way branch on `artworkSink`. `artworkSink` remains + on `DeviceTrack` as a readable introspection property used for progress + reporting and to suppress dishonest `syncTag.artworkHash` claims on noop + adapters (churn-loop guard, doc-041 §3.6). Behaviour is unchanged: iPod + bytes go to libgpod's ArtworkDB, mass-storage bytes route through the tag + writer or sidecar cover.jpg path. Internal consumers (Pipeline, repair, + diagnostics) are updated; downstream callers using `DeviceAdapter` need + only switch to the adapter methods. + +- [`01ecedd`](https://github.com/jvgomg/podkit/commit/01ecedde623ff99e94c5cbda75ff9f9c9ecef632) Thanks [@jvgomg](https://github.com/jvgomg)! - New packages: `@podkit/devices-ipod` (canonical home for iPod generation tables, model lookups, and capability synthesis) and `@podkit/devices-mass-storage` (user-extensible DAP preset framework for Echo Mini, Rockbox, generic, and custom devices). + + Echo Mini is now auto-detected at `device add` — when the USB descriptor matches the known VID/PID (`0x071b`/`0x3203`), no `--type echo-mini` flag is required. + + `enumerateConnectedDevices` is now the recommended way to discover and classify USB devices. It accepts a `providers: DeviceProvider[]` array and returns `EnumeratedDevice[]` carrying both the USB connection info and the provider-produced identity. + + `getCapabilities` in `@podkit/devices-ipod` is libgpod-free. Capability synthesis is purely table-and-firmware-driven; the legacy `createIpodCapabilities` adapter that depended on a live libgpod `LibgpodDeviceInfo` struct is deprecated in `@podkit/core`. Parity is verified across all 29 generations (the 4 that were libgpod `unknown` degenerate cases are now correctly populated from the table). + + Internal re-export shims in `@podkit/core` keep all existing call paths compiling for one release. The shims delegate to `@podkit/devices-ipod` and `@podkit/devices-mass-storage` and will be removed in P4. + +- [`667d66b`](https://github.com/jvgomg/podkit/commit/667d66b90e0979aaff381968358f2cfc78c8e581) Thanks [@jvgomg](https://github.com/jvgomg)! - Refactor the diagnostic-check scope model from a 2-field shape (`scope: 'system' | 'device'` + `category?: 'readiness' | 'database'`) to a single required 3-way union (`scope: 'system' | 'device-readiness' | 'database-health'`). Compile-time enforcement that every check declares which section it renders into; no more silent fallback when `category` is missing. The user-facing CLI `--scope` flag values are unchanged. + +- [`03f1046`](https://github.com/jvgomg/podkit/commit/03f1046b70898b0282d0c96927bca60ee0d55eeb) Thanks [@jvgomg](https://github.com/jvgomg)! - Add `podkit doctor --repair artwork-reset` to clear all artwork from an iPod without needing a source collection. This is a fast alternative to a full rebuild — useful when your source collection isn't available or you just want to clear corrupted artwork quickly. + + Rename `--repair artwork-integrity` to `--repair artwork-rebuild` to better describe what the repair does. The old name no longer works. + +- [`78b0c71`](https://github.com/jvgomg/podkit/commit/78b0c71b9866306aecbb96f2a0e372a86564f2fc) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit doctor` now renders a consistent `System` / `Device Readiness` / `Database Health` section structure across all device types. Previously, mass-storage devices (Echo Mini) collapsed everything into a single `Device Health` bucket and mis-categorised three system-scope checks. The fix audits every check's `scope` tag, adds a `category?: 'readiness' | 'database'` discriminator so device-scope checks can be routed to the right subsection, and skips `iPod Firmware Inquiry Methods` on non-iPod devices. + +- [`14d83e5`](https://github.com/jvgomg/podkit/commit/14d83e5e59eb0a8a801850de775f9fdb4c0e7aa9) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit doctor` gains `--no-system` to skip system-scope checks (FFmpeg encoders, libusb availability, udev rule). System checks remain on by default; pass `--no-system` for device-only diagnostics or in tests where the host environment shouldn't influence the result. + + The `sysinfo-consistency` check is redesigned: a missing `SysInfoExtended` file is now `skip` (not `fail`) since absence is not a failure mode. When the file is present it's compared against the live device on two independent axes — FireWireGUID and model generation — and only fails when at least one axis can be evaluated and disagrees. The check picks up live device data via the new `liveIdentity` field on `DiagnosticContext`, which `runDiagnostics` accepts as part of `RunDiagnosticsInput`. + +- [`e0f65f4`](https://github.com/jvgomg/podkit/commit/e0f65f4b0cf4fce28138849b7a85f2c3a7c1a613) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit doctor` repairs now acquire the per-device sync lock before mutating the device. + + Previously, `podkit doctor --repair ` would happily run while a `podkit sync` (or daemon-driven sync) was mid-flight against the same device. For mass-storage devices, this meant doctor's `--repair orphan-files` could prune phantom manifest entries from `state.json`, only for sync's eventual `save()` to clobber the prune from in-memory state — silently undoing the user's repair. For iPod devices, concurrent libgpod writes (artwork rebuilds, sysinfo fixes, debris cleanup) could corrupt the iTunesDB. + + The fix: every `--repair` that mutates the device now acquires the same per-device lock that `podkit sync` takes (`.podkit/sync.lock` for mass-storage, `iPod_Control/.podkit-sync.lock` for iPod). On contention, doctor exits with `LOCK_HELD` (exit code **4**) and a message naming the holding PID: + + ``` + Error: Another podkit process is using /Volumes/TERAPOD (pid 12345). Wait for it to finish or kill it. + ``` + + Audited and locked: `orphan-files`, `artwork-rebuild`, `artwork-reset`, `debris-files` (iPod), `sysinfo-extended`, `sysinfo-consistency`, `sysinfo-modelnum-mismatch`. System-only repairs that don't touch the device (`udev-rule`, `debris-transcode-tmp`) correctly skip the lock. `--dry-run` repair invocations also skip the lock — dry-run is read-only by design and matches `podkit sync --dry-run`'s policy. + + **Internal:** `resolveSyncLockPath` moved from the CLI to `@podkit/core` (exported from `lib/sync-lock-path.ts`) so doctor and sync share the same implementation. New JSDoc on `pruneManifestRows` documents the lock requirement for any future direct caller. Architecture doc `documents/architecture/sync/planning.md` §6 now enumerates every manifest-writer surface with confirmed lock semantics. + +- [`4efa15c`](https://github.com/jvgomg/podkit/commit/4efa15c7e42874e9dd88ef2731230d5314d83f20) Thanks [@jvgomg](https://github.com/jvgomg)! - Unify `--repair` IDs across device types and add debris-only diagnostic checks. + + The `podkit doctor --repair` flag now uses one ID per repair regardless of device type. Internally, the framework dispatches the right walker based on the connected device: + - `--repair orphan-files` — works on both iPod and mass-storage. (Previously `orphan-files` was iPod-only; `orphan-files-mass-storage` was the mass-storage variant.) + - `--repair debris-files` (new) — cleans podkit's own `.podkit-tmp` and adapter-failure write residue from prior interrupted syncs. Repair is safe-by-design (no confirmation prompt) because every debris file is incomplete by construction. + - `--repair debris-transcode-tmp` (new) — reaps abandoned `podkit-transcode-*` scratch directories from SIGKILLed prior syncs. Uses an mtime-based safety floor so concurrent sibling processes are never disturbed. + + **Breaking:** `--repair orphan-files-mass-storage` has been **removed**. Users running this flag will see Commander's choices() validation error listing the new public IDs (including `orphan-files`). Migration is mechanical: replace every occurrence with `--repair orphan-files`. + + The orphan check no longer reports debris in its detail output — that's the new `debris-files` check's job. Same FS walk, two checks; no double traversal. + +- [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4) Thanks [@jvgomg](https://github.com/jvgomg)! - Add SCSI firmware inquiry for iPod identification (P1 — m-18 device-capability architecture). + + `@podkit/device-types` (first published release) provides the canonical shared type definitions — `DeviceCapabilities`, `DeviceIdentity`, `ParsedFirmware`, and `DeviceProvider` — used across the podkit monorepo without circular dependencies. + + `@podkit/ipod-firmware` (first published release) implements iPod firmware inquiry via SCSI (Linux SG_IO + macOS IOKit, using koffi FFI) with USB fallback through the existing libgpod-node binding. Devices that previously failed identification over USB — including iPod mini 2G, nano 2G, and some iPod 5G Video configurations — can now be identified via SCSI. The orchestrator probes available transports at startup, prefers USB when both are available, and falls back to SCSI transparently. + + `@podkit/core` now routes `ensureSysInfoExtended` through the new orchestrator with SCSI fallback, and registers two new `podkit doctor` checks: `inquiry-methods` (reports which transports are available on this host) and `sysinfo-consistency` (validates that the on-disk SysInfo file matches the live firmware read). EACCES errors from SCSI include step-by-step recovery instructions. + + `podkit` CLI gains `--repair udev-rule` in `podkit doctor` to install the Linux udev rule that grants non-root `/dev/sg*` access, and surfaces the new doctor checks in the readiness output. + +- [`09c4acd`](https://github.com/jvgomg/podkit/commit/09c4acdec349f200a649b2db15fe05345e380a7b) Thanks [@jvgomg](https://github.com/jvgomg)! - Add canonical IpodModel type for structured device identity + - Add `IpodModel` interface — canonical representation of identified iPod model with `displayName`, `generationId`, `checksumType`, `color`, `capacityGb`, `modelNumber`, and `source` provenance + - Add `resolveIpodModel()` factory — builds an `IpodModel` from USB product ID, SysInfo model number, or serial number suffix + - Add `UsbConnectionInfo` interface — pure USB bus topology data, split from device identity + - Restructure `UsbDiscoveredDevice` to carry `usb: UsbConnectionInfo` + `model?: IpodModel` + - Add `usbModel` and `deviceModel` to `ReadinessResult` — USB-derived and SysInfo-derived models kept separate for mismatch detection + - Update `SysInfoExtendedResult` with structured `model`, `firewireGuid`, `serialNumber` fields + - Clean `checkSysInfo()` return type — new `SysInfoCheckResult` separates stage result from device model + - Add `model` to JSON output for `device scan` and `device info` commands + - `device scan` and `device info` now display richest available model name (color/capacity from SysInfo when available) + - Remove `UsbDeviceInfo` type (replaced by `UsbConnectionInfo` + `IpodModel`) + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`94c85d2`](https://github.com/jvgomg/podkit/commit/94c85d2a9d6c85875432a0ebecab540a9ebd67d7) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix `--delete` to only remove managed files on mass-storage devices, and add orphan file detection via `podkit doctor`. + + **Bug fix:** `--delete` previously removed all unmatched files on mass-storage devices, including user-placed files. It now only removes files that podkit manages (tracked in `.podkit/state.json`), matching iPod behavior where only database tracks are candidates for deletion. + + **Collision detection:** Sync now detects when a planned file write would collide with an existing unmanaged file and reports the conflict before writing. Works in both normal sync and `--dry-run` mode. + + **New diagnostic check:** `podkit doctor` now runs health checks on mass-storage devices. The `orphan-files-mass-storage` check detects unmanaged files in content directories and can clean them up via `podkit doctor --repair orphan-files-mass-storage`. + + **Other improvements:** + - State manifest (`.podkit/state.json`) is now written without pretty-printing to reduce file size on device storage + - Shell completions now include valid repair IDs for the `--repair` option + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`efa14c6`](https://github.com/jvgomg/podkit/commit/efa14c623e7bda81066bd77142cddb28e4de615d) Thanks [@jvgomg](https://github.com/jvgomg)! - Add mass-storage device support for non-iPod portable music players. + + **Supported device types:** Echo Mini, Rockbox, and generic mass-storage DAPs. iPod support is unchanged. + + **New in CLI (`podkit`):** + - `podkit device add --type ` registers mass-storage devices by type and mount path + - `podkit device info/music/video` work with mass-storage devices via `DeviceAdapter` interface + - `podkit device scan` shows configured path-based devices alongside auto-detected iPods + - `podkit sync` routes to the correct adapter (iPod or mass-storage) based on device config + - Video sync now uses capabilities-based gating instead of iPod-only checks + - Safety gates on `device init/reset/clear` (iPod-only commands) for mass-storage devices + - Mount and eject commands show device-appropriate messaging + - Config validation rejects capability overrides on iPod devices (capabilities are auto-detected from generation) + - Shared `openDevice()` function eliminates duplicated device-opening logic across commands + + **New in core (`@podkit/core`):** + - `DeviceAdapter` interface — generic abstraction over device databases (iPod, mass-storage) + - `MassStorageAdapter` — filesystem-based track management with `.podkit/state.json` manifest + - `IpodDeviceAdapter` — thin wrapper making `IpodDatabase` implement `DeviceAdapter` + - Device capability presets for Echo Mini, Rockbox, and generic devices + - `resolveDeviceCapabilities()` merges preset defaults with user config overrides + - `DeviceTrack` type used throughout sync engine (replaces `IPodTrack` casts in execution paths) + - Configurable content path prefixes (`musicDir`, `moviesDir`, `tvShowsDir`) with device-type defaults + - Device presets include default content paths (Echo Mini: root for music; generic/Rockbox: `Music/`, `Video/Movies/`, `Video/Shows/`) + - Manifest v2 stores active content paths; files automatically moved when prefixes change + - Root path support (`/`, `.`, or empty string all normalize to device root) + - Content path duplicate validation (no two content types can share the same prefix) + - Video scanning support for mass-storage devices (.m4v, .mp4, .mov, .avi, .mkv) + + **New in daemon (`@podkit/daemon`):** + - Mass-storage device polling via `PODKIT_MASS_STORAGE_PATHS` env var (colon/comma separated) + - Second `DevicePoller` + `SyncOrchestrator` pair for mass-storage devices + - No-op mount/eject runners (mass-storage devices are externally managed) + - Graceful shutdown handles both iPod and mass-storage sync pipelines + + **Configuration:** + + ```toml + [devices.echo] + type = "echo-mini" + path = "/Volumes/ECHO" + + # Optional capability overrides (mass-storage only) + artworkMaxResolution = 800 + supportedAudioCodecs = ["aac", "mp3", "flac"] + + # Optional content path overrides (mass-storage only) + musicDir = "/" # Place music at device root + moviesDir = "Films" # Custom movies directory + tvShowsDir = "TV Shows" # Custom TV shows directory + ``` + + **Environment variables for content paths:** + - `PODKIT_MUSIC_DIR` — global default music directory + - `PODKIT_MOVIES_DIR` — global default movies directory + - `PODKIT_TV_SHOWS_DIR` — global default TV shows directory + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`208e482`](https://github.com/jvgomg/podkit/commit/208e482db9730064a25e53e03121bdcfcbea6341) Thanks [@jvgomg](https://github.com/jvgomg)! - Embed artwork in OGG/Opus files for mass-storage devices with `artworkSources: ['embedded']`. + + FFmpeg's OGG muxer cannot write image streams (upstream tickets [#4448](https://github.com/jvgomg/podkit/issues/4448), [#9044](https://github.com/jvgomg/podkit/issues/9044), open since 2015), so OGG output was previously stripped of artwork with `-vn`. Mass-storage devices relying on embedded artwork showed no cover art for Opus tracks. + + **What changed:** + - After FFmpeg produces an OGG file (with artwork stripped), the pipeline post-processes it via node-taglib-sharp to embed artwork as a `METADATA_BLOCK_PICTURE` Vorbis comment + - Artwork is resized to the device's `artworkMaxResolution` before embedding, matching the behavior of other formats where FFmpeg handles resize during transcode + - Resize results are cached per-album to avoid redundant FFmpeg image-processing spawns + - New `TagWriter.writePicture()` method and `resizeArtwork()` utility + - Pending picture writes follow the same deferred flush pattern as comment and ReplayGain tag writes (queued by `updateTrack`, flushed by `save()`) + +- [`f5d0082`](https://github.com/jvgomg/podkit/commit/f5d00829f3b1a80453bdc4f7e6599566f7f02bb3) Thanks [@jvgomg](https://github.com/jvgomg)! - Reconcile USB-inquiry and block-device discovery so each connected iPod renders once in `podkit device scan`. Previously, `device scan` could surface the same physical iPod twice on Linux when both pipelines independently identified it. The orphan entry also surfaced a destructive remediation (`Needs partitioning — see: podkit device init`) on a healthy device. Both issues fixed: a new reconciliation primitive matches USB and block-device records by serial number (or disk identifier as fallback), and the readiness-failure copy now points at docs instead of suggesting an inappropriate command. + +- [`4ee5e2b`](https://github.com/jvgomg/podkit/commit/4ee5e2be470a93a54c2d54bc0aab257d7b92babe) Thanks [@jvgomg](https://github.com/jvgomg)! - Refuse HFS+ iPods on Linux at `device add`; warn at `device scan` + + iPods formatted as HFS+ are now refused on Linux at `podkit device add` time, with a clear message pointing at docs explaining how to reformat to FAT32. `podkit device scan` surfaces the same iPods with a `Filesystem not supported on Linux` warning instead of running readiness stages or suggesting destructive remediation. macOS HFS+ behaviour is unchanged. + + Why: the Linux kernel hfsplus driver refuses RW on journaled HFS+ (the iPod default), udev/blkid don't surface a filesystem UUID for HFS+ on Linux (breaking podkit's identity model), and udisksctl mount paths fall back to a generic name with no label. Each friction point has a partial fix; together they mean Linux + HFS+ is a second-class experience no matter how much we patch. Refusing cleanly with a docs link sharpens podkit's Linux story to "FAT32 iPods, supported well." + + Structured `--json` output preserves a stable error code (`UNSUPPORTED_FILESYSTEM_ON_LINUX`) so scripted callers can handle the refusal. + +- [`303c35a`](https://github.com/jvgomg/podkit/commit/303c35aea57c0f35f64481e12e5cb9298e9a5631) Thanks [@jvgomg](https://github.com/jvgomg)! - Remove `executeMusicPlan` library convenience. + + `executeMusicPlan` bypassed the engine `SyncExecutor` and carried subtly different save semantics (no checkpoint cadence) after ADR-019 P1 landed. Library callers should drive `MusicPipeline` through `createSyncExecutor(createMusicHandler(...))` for engine-owned save coordination, or instantiate `new MusicPipeline(deps)` and iterate `execute()` directly for minimal aggregation needs. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`bb96778`](https://github.com/jvgomg/podkit/commit/bb96778dde9063267188b2b83535ec279cd5c550) Thanks [@jvgomg](https://github.com/jvgomg)! - Write ReplayGain tags to transcoded files for mass-storage devices with `audioNormalization: 'replaygain'` (e.g., Rockbox). + + Previously, ReplayGain data was only stored as iPod soundcheck values in the iTunes database. Mass-storage devices read volume normalization from file tags, but FLAC→AAC transcoding strips ReplayGain metadata. Tracks on Rockbox devices played without normalization. + + **What changed:** + - ReplayGain tags (`REPLAYGAIN_TRACK_GAIN`, `REPLAYGAIN_TRACK_PEAK`) are injected via FFmpeg `-metadata` flags during transcoding for MP3, FLAC, and OGG/Opus output + - M4A files (where FFmpeg can't write ReplayGain metadata) get tags written via node-taglib-sharp after transfer + - Raw ReplayGain dB/peak values are preserved from collection sources (Subsonic API, local files) through the sync pipeline, avoiding precision loss from soundcheck integer conversion + - Device scan reads ReplayGain from file tags so the sync engine can detect when normalization data changes and needs updating + - Direct-copy operations skip tag writing since source files already have correct tags + - `soundcheckToReplayGainDb()` reverse conversion function added for back-converting when raw values aren't available + + **Bug fix:** `IpodTrackImpl` used `data.soundcheck || undefined` which coerced a valid soundcheck of `0` to `undefined`. Changed to `data.soundcheck ?? undefined`. + +- [`275c972`](https://github.com/jvgomg/podkit/commit/275c97295462547037e2c911c139654eb50d4af7) Thanks [@jvgomg](https://github.com/jvgomg)! - Sidecar artwork support and executor adapter fallback. + + The sync pipeline now picks up out-of-band artwork that lives alongside the audio file or on the source server, not just embedded pictures: + - **Directory adapter** detects peer `cover.jpg` / `folder.jpg` / `front.jpg` / `album.jpg` (also `.jpeg` / `.png`, case-insensitive) in the same directory as the audio file. When a file has no embedded picture, the sidecar bytes are used. Under `--check-artwork` the sidecar bytes are hashed and pinned in the sync tag, so swapping a `cover.jpg` for a new image is detected on the next sync. + - **Subsonic adapter** falls back to Navidrome's `getCoverArt` endpoint when the downloaded audio file body has no embedded picture. This closes the gap where a Navidrome library indexed sidecar art on the server but podkit silently dropped it on every sync. + - A one-time placeholder probe runs on every Subsonic `connect()` so Navidrome's static "no cover" image is filtered regardless of `--check-artwork`. + + Embed-in-the-file wins when both are present; the sidecar / API fallback only fires on a miss. Album-level caching means siblings on the same album share a single sidecar read or API request. + + Adapters gain an optional `getArtwork(item): Promise` method on `CollectionAdapter`. The executor calls it through the existing `AlbumArtworkCache` after embedded extraction returns null. + + **Known gap (deferred to TASK-370 / TASK-371 / TASK-372):** mass-storage devices accept the bytes via `setArtworkFromData`, which is a no-op for non-OGG/Opus containers — adapter-fallback bytes reach the device only when the output is OGG/Opus copy (via the existing taglib path) OR the target is an iPod (via the iTunesDB). For other mass-storage outputs the bytes are dropped silently today; the e2e artwork matrix fences those cells with a `[BUG] TASK-370` skip rather than failing the suite. + +- [`cac7fc1`](https://github.com/jvgomg/podkit/commit/cac7fc123861e97b10d31c83728a1e3f0431934e) Thanks [@jvgomg](https://github.com/jvgomg)! - Sidecar lifecycle cleanup + broader orphan-files surface (TASK-375). + + **Sync-time sidecar cleanup (mass-storage, sidecar-primary devices).** + `MassStorageAdapter.removeTrack` and `relocateTrack` now drop the + album's `cover.jpg` when the last managed audio file leaves the + directory. The delete is queued at the moment of removal and flushed + in a new `save()` stage that re-evaluates the predicate per entry, so + a re-add inside the same save cycle (whether through `writeSidecar` or + via a pipeline that skipped artwork on a hash match) cleanly cancels + the queued delete. The previous behaviour left a dangling `cover.jpg` + and a stale manifest entry forever; sync-time cleanup keeps the + invariant "every managed sidecar has at least one managed audio + sibling in its dir" alive. + + **Doctor's orphan-files check no longer filters by extension.** The + mass-storage walker previously surfaced only audio/video files as + orphan candidates. Any other file in your content directories + (sidecar images, lyrics `.lrc`, playlist `.m3u`, stray documents) was + silently dropped. Now the check considers any non-debris file in the + configured content roots — confirmation-gated repair stays unchanged + so you review the list before anything is deleted. This is the + backstop for sidecars on devices that synced before sync-time cleanup + existed, and surfaces unmanaged user-placed files in podkit's + territory you may want to clear out. + + **Migration:** on a rockbox device with a pre-existing user-placed + `cover.jpg` that podkit never wrote, the orphan check will now flag + it. The confirmation prompt is your safety; review before repairing. + After deletion, the next sync re-issues a managed sidecar. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`f72fa01`](https://github.com/jvgomg/podkit/commit/f72fa0170872fc0a6e5719b4509abae24e6414cd) Thanks [@jvgomg](https://github.com/jvgomg)! - Refactor sync engine to be fully content-type-agnostic with per-handler operation types. + + **Breaking:** `createMusicHandler()` and `createVideoHandler()` now take a config object at construction instead of using `setTransformsConfig()`/`setExecutionConfig()`. Removed `HandlerDiffOptions`, `HandlerPlanOptions`, `MusicExecutionConfig` types. Renamed `MusicExecutor` to `MusicPipeline`. Removed legacy planner functions (`createMusicPlan`, `planVideoSync` and related helpers). + + **New:** `MusicSyncConfig`, `VideoSyncConfig`, `MusicTrackClassifier`, `VideoTrackClassifier`, `MusicOperationFactory`, `MusicOperation`, `VideoOperation`, `BaseOperation` types. Handlers now own their operation types via `TOp` type parameter on `ContentTypeHandler`. + +- [`7bf7127`](https://github.com/jvgomg/podkit/commit/7bf7127d3141ce4b91138e3284b18aa5e8ea5984) Thanks [@jvgomg](https://github.com/jvgomg)! - Unify sync-engine error and warning handling (architecture sweep) + + Settles error and warning responsibilities across the sync engine. Hard + failures throw typed errors that carry their own category; soft signals + flow through an injected `WarningSink` and surface alongside hard errors + in `SyncOutput.warnings`. `console.warn` / `console.error` is now banned + in core. + + See `documents/architecture/sync/error-handling.md` for the full + responsibility model. + + ## Breaking API changes + + ### Types + - **`SyncWarning` and `ExecutionWarning` types removed.** Replaced by a + single `Warning` type with `phase: 'plan' | 'execute'`. Track + references are now structured (`WarningTrackRef = {artist, title, album?}`) + rather than a mix of `CollectionTrack[]` and an inline object. + + ```ts + // before + import type { SyncWarning, ExecutionWarning } from '@podkit/core'; + // after + import type { Warning, WarningPhase, WarningType, WarningTrackRef } from '@podkit/core'; + ``` + + - **`SyncPlan.warnings`** is now `Warning[]` (always `phase: 'plan'`). + - **`ExecuteResult.warnings`** is now `Warning[]` (always `phase: 'execute'`). + - **`CollectionAdapter.getPlanWarnings?()`** now returns `Warning[]`. + + ### New types + - **`Warning`, `WarningPhase`, `WarningType`, `WarningTrackRef`, + `WarningSink`** — the unified warning surface. + - **`CategorizedSyncError`** — abstract base class for all typed sync + errors. Subclasses declare `readonly category: ErrorCategory` so the + pipeline's categorizer reads it off the class instead of inspecting + the message body. + - **`DatabaseWriteError`** — wraps libgpod failures at the + `IpodAdapter` boundary so iTunesDB errors categorize as `database` + (no retry) rather than falling through to op-type fallback. + - **`PictureWriteError`**, **`MoveError`** — typed siblings of the + existing `TagWriteError` / `SidecarWriteError`, now also extending + `CategorizedSyncError`. + + ### `DeviceAdapter` contract + - New optional **`setWarningSink(sink: WarningSink): void`** method. + Adapters that emit execute-phase warnings (`IpodAdapter`, + `MassStorageAdapter`) must implement it. The pipeline injects its + accumulator sink at execute start. + + ## Breaking CLI JSON output changes + + The `sync` command's JSON output replaces the prior two warning fields + with a single unified array: + + ```diff + { + "success": true, + - "planWarnings": [{ "type": "lossy-to-lossy", "message": "...", "trackCount": 2, "tracks": [...] }], + - "executionWarnings": [{ "type": "artwork", "track": "Artist - Title", "message": "..." }] + + "warnings": [ + + { + + "phase": "plan", + + "type": "lossy-to-lossy", + + "message": "...", + + "trackCount": 2, + + "tracks": [{"artist": "...", "title": "...", "album": "..."}] + + }, + + { + + "phase": "execute", + + "type": "artwork", + + "message": "...", + + "trackCount": 1, + + "tracks": [{"artist": "...", "title": "...", "album": "..."}] + + } + + ] + } + ``` + + Filter by `warning.phase` to recover the prior split. Track refs + inside warnings are now structured objects rather than pre-formatted + strings — consumers can format them as they wish. + + ## Behaviour fixes + - **Execute-phase warnings now surface in `--json`.** The + `executionWarnings` field was declared on `SyncOutput` but never + populated by the CLI's real-run path — artwork extraction failures, + iPod portable tag-write misses, and mass-storage vanished-relocate + events were accumulated by the pipeline and silently dropped before + reaching JSON. They now appear in the unified `warnings` array. + - **`IpodAdapter` mutators (`addTrack`, `updateTrack`, `removeTrack`) + wrap libgpod failures in `DatabaseWriteError`.** Without the wrap, + libgpod errors during these mutators would categorize as `copy` via + the op-type fallback and retry once. iTunesDB failures now correctly + categorize as `database` (no retry). + - **Mass-storage picture-write stage normalized to collect-and-aggregate.** + Was `Promise.all` fail-fast with an untyped rejection; now + `runWithConcurrency` + settled-all + `PictureWriteError` + map-cleared- + before-throw, matching the tag-write stage convention. + - **CLI text-mode now prints an execute-phase warning summary** at the + end of a real sync run (grouped by warning type; expand with `-v`). + Previously these warnings were invisible to text-mode users. + + ## New text-mode CLI behaviour + + A new `Warnings:` block appears in the sync summary when execute-phase + warnings landed during the run: + + ``` + === Summary === + + Synced 152 items successfully + Duration: 8m 14s + + Warnings: 3 + artwork: 2 + tag-write: 1 + (re-run with -v for details) + ``` + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`c9c268e`](https://github.com/jvgomg/podkit/commit/c9c268ea4b25b39543e5c53a1928e72b4c31e0c8) Thanks [@jvgomg](https://github.com/jvgomg)! - Internalize raw sync tag functions (`parseSyncTag`, `formatSyncTag`, `writeSyncTag`) from the public API. Sync tag reads now use the typed `DeviceTrack.syncTag` field, and writes use `DeviceAdapter.writeSyncTag()` or the new `update-sync-tag` operation type. Adds `SyncTagUpdate` type and `syncTagsEqual` to the public API. + +- [#59](https://github.com/jvgomg/podkit/pull/59) [`1ec30ac`](https://github.com/jvgomg/podkit/commit/1ec30acca1109178012db3913a60967a2087fb5b) Thanks [@jvgomg](https://github.com/jvgomg)! - Automated iPod device identification via SysInfoExtended. + + Modern iPods (post-2006) ship without a populated `SysInfo` file after iTunes restore. Without it, libgpod treats the device as generic — artwork breaks, ALAC support is unknown, and database checksums fail on Classic 6/7G and Nano 3G+. podkit now reads SysInfoExtended directly from iPod firmware over USB during `device add`, so first-time setup works with no manual tooling. + + **User-visible:** + - `podkit device add` identifies the exact model (e.g. "iPod nano 8GB Black (3rd Generation)") with no input + - `podkit doctor` detects missing SysInfoExtended and offers `--repair sysinfo-extended` + - `podkit sync` works correctly on first run with full capability detection + - Hash72 (Nano 5G) and HashAB (Nano 6G) devices get clear limitation messages + - SysInfoExtended write is gated on user confirmation during `device add` + + **Core (`@podkit/core`):** + - Unified iPod model registry — single table, both `0x120x`/`0x126x` USB ID ranges, 190+ serial-suffix → model mappings, checksum-type classification per generation + - `ensureSysInfoExtended()` orchestrator: check existing → USB read → validate XML → write + - USB discovery now exposes `serialNumber`, `busNumber`, `deviceAddress`; `resolveUsbDeviceFromPath()` on macOS + Linux + - Readiness pipeline: checksum-aware severity (hash58+ devices fail without SysInfoExtended; pre-checksum devices warn) + - `READINESS_RULES` declarative array replaces ad-hoc `determineLevel()` logic + - New `sysinfo-extended` diagnostic check + - Recognizes `P` / `F` model prefixes in SysInfo + + **libgpod-node (`@podkit/libgpod-node`):** + - `readSysInfoExtendedFromUsb()` N-API binding, resolved via `dlsym` at runtime so it loads gracefully on systems where libgpod lacks the symbol + - Prebuild patches upstream libgpod 0.8.3 to move `itdb_usb.c` from `tools/` into the library; libusb 1.0.27 built from source on all 6 platforms + - `--whole-archive` / `-force_load` linker flags preserve the dlsym symbol in the `.node` binary + + **CLI (`podkit`):** + - `device add` attempts SysInfoExtended read after mount, before DB init; enriches model name in summary + - `doctor` adds suggested-actions section, drops destructive sysinfo guidance + - `device scan` and `doctor` show clearer SysInfo readout + +- [`c5cba69`](https://github.com/jvgomg/podkit/commit/c5cba6998283663b42659f02b17b194ab256c137) Thanks [@jvgomg](https://github.com/jvgomg)! - Polish on the convergent-metadata work (TASK-327 follow-up): + - **Tag-write concurrency cap.** `save()` now caps in-flight tag writes at 16 via a small `runWithConcurrency` helper instead of firing every pending write at once. Avoids `EMFILE` on large libraries. + - **Aggregated tag-write errors.** Failure messages now begin with `tag write failed` so the executor's error categorizer classifies them as file-I/O (`copy`) rather than risking a path-keyword mis-classification. + - **WAV/AIFF on mass-storage.** Podkit transcodes WAV and AIFF source files to a managed codec before placing them on a mass-storage device, even when the device firmware can play them. RIFF/IFF tag-writing is unreliable. Presets continue to list these codecs for documentation. iPod is unaffected (libgpod / iTunesDB handle metadata for WAV/AIFF). + - **OGG Vorbis tag round-trip tests.** Now run on builds with libvorbis (skipped automatically when absent). + - **Shared TagFields helpers.** `buildTagFieldsFromInput` and `diffTagFields` replace three duplicate field-by-field walks across adapters. + - **`TransferMode` type unified.** Removed `'fast' | 'optimized' | 'portable'` duplication between `DeviceTrackInput`, `DeviceTrackMetadata`, and the canonical `TransferMode` in `transcode/types.ts`. Drops several inline type casts. + - **Docs.** `transferMode` now has a dedicated section in `docs/reference/config-file.md` explaining the iPod vs mass-storage contract and migration churn. `pathTemplate` (from the prior release) and `PODKIT_PATH_TEMPLATE` are now documented in the config reference and environment-variables reference. + +- [`1c3ebc3`](https://github.com/jvgomg/podkit/commit/1c3ebc381276accdb8361f50454b90c75f2391df) Thanks [@jvgomg](https://github.com/jvgomg)! - Add three-tier transfer mode system controlling how files are prepared for the device. + + **Transfer modes:** + - `fast` (default): optimizes for sync speed — direct-copies compatible files, strips artwork from transcodes + - `optimized`: strips embedded artwork from all file types (including MP3, M4A, ALAC copies) via FFmpeg stream-copy, reducing storage usage without re-encoding + - `portable`: preserves embedded artwork in all files for use outside the iPod ecosystem + + **Configuration:** + - `transferMode` config option (global and per-device) + - `--transfer-mode` CLI flag + - `PODKIT_TRANSFER_MODE` environment variable + + **Selective re-processing:** + - `--force-transfer-mode` flag re-processes only tracks whose transfer mode doesn't match the current setting + - `PODKIT_FORCE_TRANSFER_MODE` environment variable + - Works on all file types including direct copies (unlike `--force-transcode` which only affects transcoded tracks) + + **Device inspection:** + - `podkit device music` and `podkit device video` stats show transfer mode distribution + - Missing transfer field flagged alongside missing artwork hash in sync tag summary + - New `syncTagTransfer` field available in `--tracks --fields` for querying transfer mode data + - Dry-run output shows configured transfer mode + + **Under the hood:** + - Granular operation types: `add-direct-copy`, `add-optimized-copy`, `add-transcode` (and upgrade equivalents) + - Sync tags written to all tracks including direct copies (`quality=copy`) + - `DeviceCapabilities` abstraction for device-aware sync decisions + - Sync tag field `transfer=` tracks which mode was used per track + +- [`ec8dc85`](https://github.com/jvgomg/podkit/commit/ec8dc8549447b0178a8746b8cda2b8b7908b9d04) Thanks [@jvgomg](https://github.com/jvgomg)! - Unify the unsupported-device UX across `podkit device add`, `device scan`, `device info`, `sync`, and `doctor`. Every command now composes identity via the same cascade primitive (`resolveIpodModel(bag)`) — no command re-implements the check, no command leaks `libgpod` into user-facing copy. + + Key behaviour changes: + - `device add` on an unsupported device (hashAB nano, etc.) now asks "Add anyway? [y/N]" rather than hard-refusing. Confirmed devices are recorded with `unsupported: true` in config; `--yes` flips the default to accept. + - `device add` against an iOS device (iPod touch) now surfaces the canonical unsupported message instead of the generic "No iPod devices found". + - `device scan` headers show the resolved model name (e.g. "iPod touch 5th generation") instead of "Unknown iPod (USB only)". + - `sync --dry-run` refuses cleanly on unsupported devices with the canonical message — no track plan generated. + - `sync` on a supported device with SysInfoExtended present resolves identity via the cascade; the legacy "Could not identify iPod model" warning is gone for that case. + - `device info` renders the cascade `displayName` instead of the libgpod-derived `info.device.modelName`. + - `doctor` on an unsupported device suppresses repair suggestions that would mutate device state and surfaces the canonical unsupported message instead. + + Wording is centralised in `@podkit/core` (`makeUnsupportedReasonFromAssessment` / `makeUnsupportedReasonFromModel`) — every consumer imports. + +- [`e825ee1`](https://github.com/jvgomg/podkit/commit/e825ee1dd4933ecbfd070dda27f96f43056f0baf) Thanks [@jvgomg](https://github.com/jvgomg)! - Replace koffi-based libusb FFI with the `usb` npm package for USB firmware inquiry, eliminating the runtime libusb system dependency. + + The `@podkit/ipod-firmware` USB transport now uses the `usb` npm package, whose prebuilt N-API bindings statically link libusb. End-user binaries embed that prebuild via Bun `--compile`; no system `libusb-1.0` is required at runtime. + + Public-surface changes in `@podkit/ipod-firmware`: + - **Removed:** `loadLibusb`, `LibusbBinding`, `LibusbPtr`, `LibusbLoadResult`, `_resetLibusbCacheForTests`. The koffi-shaped binding interface is gone. + - **Added:** `loadUsb`, `UsbBinding`, `UsbDeviceHandle`, `UsbLoadResult`, `_resetUsbCacheForTests`. Higher-level `withOpenDevice(bus, devnum, fn)` seam — implementations handle enumeration, open, and cleanup internally. + - **Added:** `setLogger(fn | null)`, `FirmwareLogger`, `FirmwareLogEvent`. Library no longer writes to stderr/stdout; consumers install a receiver and decide format/destination. The CLI installs one when `-v` is passed. + - **Added:** `@podkit/ipod-firmware/bundle` subpath export with `bundleUsbNative(nativeModule)` for single-file binary builds. See `agents/ipod-firmware.md` for the staging recipe. + - **Renamed:** `UsbInquiryError.libusbCode` → `UsbInquiryError.libusbStatus`. The new field carries `LIBUSB_TRANSFER_*` status codes (positive enum) from the `usb` npm package, not the negative `LIBUSB_ERROR_*` codes the koffi path returned. + + Doctor's `inquiry-methods` check no longer reports libusb availability — the USB transport is bundled and always present in shipped binaries. The check now reports SCSI transport availability only, which remains user-actionable on Linux (udev permissions) and macOS (iPodDriver.kext). + +### Patch Changes + +- [`621b10a`](https://github.com/jvgomg/podkit/commit/621b10abbec3a8e369da9620733210fef4b76f99) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix two artwork/codec sync bugs surfaced by the art-matrix test suite: + - **Album artwork is now deterministic regardless of track scan order.** The album-level artwork cache previously remembered the first track's extraction result for the whole album — so whether a WAV/OGG/Opus track ended up with the album cover depended on which sibling was processed first (and differed between the directory and Subsonic adapters). The cache now pre-resolves each album from a preference-ordered candidate list (embed-capable containers first) and no longer caches a negative result in single-source mode, so every track in an album inherits the same cover. + - **MP3 no longer fires a spurious `codec-changed` re-copy on every sync.** `postProcessCodecChanges` assumed any lossy source would be transcoded to the resolved lossy codec; for an MP3 source on an MP3-capable device the classifier actually direct-copies, so the codec comparison fired `upgrade-direct-copy: codec-changed` on each incremental sync. The pass now asks the classifier and skips when the source is copied rather than transcoded. + +- [`348f2c5`](https://github.com/jvgomg/podkit/commit/348f2c53cec06598903b5cf128663d5121c46865) Thanks [@jvgomg](https://github.com/jvgomg)! - Redesign `podkit device add` to be slick and informative. Previously, plugging in a post-2006 iPod (nano 2G, nano 7G, iPod 5G) and running `device add` displayed the device as `Model: Invalid` (libgpod's wording for an empty SysInfo file) and instructed the user to manually write a SysInfo file with `ModelNumStr: MA147` — neither friendly nor accurate. + + The new flow: + 1. **Identity is cascade-resolved** from USB product ID, classic SysInfo, SysInfoExtended, and serial — whichever sources are available. Display reads `Found iPod nano (2nd Generation):` rather than `Model: Invalid`. + 2. **A single combined prompt** asks `Add this iPod as "X" and write SysInfoExtended? [Y/n]` when SysInfoExtended is missing and USB is reachable. Confirming triggers firmware inquiry, writes SysInfoExtended, and persists to config in one step. + 3. **Capabilities are derived from the cascade-resolved generation**, not from libgpod's pessimistic fallback. Negative capabilities cite the reason (`- Video (not supported on iPod nano 4GB Green (2nd Generation))`). + 4. **The follow-up tip** suggests `podkit sync -d --dry-run`, not "go run two more commands". + + New flag `--no-firmware-inquiry` skips the firmware fetch+write when used with `--yes` — for the case where the user wants to defer the write or doesn't have the device connected over USB. + + Internal API changes in `@podkit/core`: + - **Added** `assessIpodIdentity(mountPoint, opts?)` returning `IpodIdentityAssessment` — pure cascade-driven assessment (no writes). Combines all available identification sources and returns `{ model, capabilities, firmwareInquiry: 'present' | 'missing' | 'unwritable', needsChecksum }`. The CLI now composes from this primitive instead of reaching into libgpod for identity. + + The misleading `device-validation.ts` warning text (`Ensure /Volumes/X/iPod_Control/Device/SysInfo exists with your model number (e.g., "ModelNumStr: MA147")`) has been replaced with a pointer to the canonical fix: `podkit doctor --repair sysinfo-extended`. + +- [`6747667`](https://github.com/jvgomg/podkit/commit/6747667049cd793fdb13e3d1bc1092651f8e969c) Thanks [@jvgomg](https://github.com/jvgomg)! - Improve device command output: USB model in scan, SysInfo mismatch detection, summary/issues layout + - `podkit device scan` now shows the USB-detected iPod model (e.g., "iPod Classic 6th generation (USB)") and always runs USB discovery in parallel with disk scanning + - `podkit device scan` and `podkit doctor` detect generation mismatches between SysInfo and USB data, warning when the SysInfo file may have been copied from a different device + - `podkit device info`, `podkit device scan`, and `podkit doctor` now separate compact check summaries from detailed issue explanations — warnings and fix commands appear in a dedicated "Issues" section instead of inline + - New `lookupGenerationByModelNumber()` function in `@podkit/core` for resolving iPod generation from SysInfo model numbers + +- [`a78e5fe`](https://github.com/jvgomg/podkit/commit/a78e5fee4e47293c1935395bb157cb6574782625) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix three `podkit doctor` repair correctness bugs: + - `--repair sysinfo-consistency` now overwrites a stale on-disk SysInfoExtended (previously short-circuited on file existence, reporting success without rewriting). + - `--repair sysinfo-extended` no longer requires an existing iTunesDB — repairs without a `database` requirement skip the DB open so identity-populating repairs work on freshly formatted iPods. New `'database'` value on `RepairRequirement`. + - The readiness `SysInfoExtended:` status line distinguishes a missing file from a present-but-unparseable one. + +- [`785ad57`](https://github.com/jvgomg/podkit/commit/785ad57af6627059fe3a6d7e1fef475e82c34764) Thanks [@jvgomg](https://github.com/jvgomg)! - `podkit doctor` (artwork-rebuild repair) now runs a per-track source-file validity probe (stat + 16-byte magic-byte header check) before the album-cache lookup. Corrupt or unreadable source files always land in the `errors` bucket with a structured reason (`missing | unreadable | truncated | badMagic`) rather than inheriting a sibling track's cache success non-deterministically. + + The `details.errorDetails[*]` in doctor's JSON output now carries optional `path` and `reason` fields so users can act on specific bad files. Backward-compatible: existing `artist` / `title` / `error` fields are preserved. + + Magic-byte signatures cover FLAC, OGG/Opus, MP3 (ID3 and bare MPEG sync), MP4/M4A/AAC, WAV, AIFF/AIFC — matching the directory adapter's accepted extensions. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`3db3d88`](https://github.com/jvgomg/podkit/commit/3db3d887ae2cd19d01ba2c1f00b8682e783fac84) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix multiple bugs discovered during end-to-end Echo Mini hardware validation + + **Sync pipeline:** + - Create temp directory for optimized-copy operations (not just transcodes), fixing "No such file or directory" FFmpeg failures on mass-storage devices + - Capture last 1000 chars of FFmpeg stderr (instead of first 500) so actual errors aren't swallowed by the version banner + + **Device preset content paths:** + - Pass device preset content paths to adapter even when no user overrides exist, fixing Echo Mini's `musicDir: ''` being ignored and files landing in `Music/` instead of device root + + **Artwork:** + - Read embedded artwork during mass-storage device scan (`skipCovers: false`) so artwork presence is correctly detected, preventing false `artwork-added` upgrades on every sync + - Force `yuvj420p` (4:2:0) pixel format in artwork scale filter — JPEG with 4:4:4 chroma subsampling does not display on the Echo Mini + + **Sync tag and preset detection:** + - Treat `quality=copy` sync tags as in-sync when the classifier would also route the source as a copy, preventing false preset-upgrade detection on FLAC-capable mass-storage devices + - Route lossless sources to transcode (not copy) when quality preset is non-lossless, even if the device natively supports the source codec (e.g., FLAC device with quality=high should produce AAC) + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`7ebb7c5`](https://github.com/jvgomg/podkit/commit/7ebb7c5c0e1c7c3d549196347029d9ce660fcb8b) Thanks [@jvgomg](https://github.com/jvgomg)! - Use configurable device label in eject messages instead of hardcoded 'iPod' + +- [`34e8bf2`](https://github.com/jvgomg/podkit/commit/34e8bf2341111df1e8f85361b8047eed9f31665a) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix incorrect device model data: correct checksum types (nano 7G: hashAB, touch 1G-3G: hash72), fix USB product IDs (0x1205: iPod mini, 0x1209: iPod Video, 0x120a: nano 1G), reclassify model B867 from nano 4G to shuffle 3G, add 15 nano 7G model number variants, add missing touch 4G serial suffixes, and add first crowd-sourced nano 7G serial suffix mapping + +- [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix iPod model identification regressing to "Unknown iPod" after `doctor --repair sysinfo-extended` on pre-2006 devices (mini 2G), and tighten the package-boundary contract so consumers compose identity instead of injecting resolution policy. + + The bug: each consumer of `ensureSysInfoExtended` / `readSysInfoExtended` passed a serial-only `resolveModel` callback. When the 3-character serial suffix wasn't in `tables/serials.ts`, the resolver returned undefined and the device was displayed as "Unknown iPod" — even when a SysInfo file with a known `ModelNumStr` was sitting next to the SysInfoExtended on disk. + + The fix: + - **Removed** `ModelResolver` type and the `resolveModel` callback from `@podkit/ipod-firmware`. `readSysInfoExtended` and `ensureSysInfoExtended` now return a flat `SysInfoIdentity` bag (`firewireGuid?, serialNumber?, modelNumStr?, familyId?`). When a SysInfo file is on disk alongside SysInfoExtended, its `ModelNumStr` is read opportunistically. + - **Callers compose** with `resolveIpodModel(bag)` from `@podkit/devices-ipod`, which cascades modelNumStr → serial → productId → familyId → libgpodGeneration. The CLI no longer makes resolution decisions. + - **Added** `SYSINFO_PATH`, `SYSINFO_EXTENDED_PATH`, `SYSINFO_DEVICE_DIR` exported from `@podkit/ipod-firmware` and re-exported from `@podkit/core`. Consumers use these constants instead of duplicating the literal `iPod_Control/Device/...` paths. + - **Added** `S4G: '9804'` entry to `tables/serials.ts` (mini 2G 4GB Pink, sourced from real hardware, serial `JQ5141TFS4G`). + - **Post-write enrichment.** After `ensureSysInfoExtended` writes the file via USB inquiry, it now re-reads via `readSysInfoExtended` so the post-write identity bag includes `modelNumStr` from the SysInfo neighbour. Eliminates the cosmetic regression where the repair-success message showed a less-specific name than the subsequent `doctor` run. + +- [`151152a`](https://github.com/jvgomg/podkit/commit/151152ae835529730b3235a780550ec35ad685e2) Thanks [@jvgomg](https://github.com/jvgomg)! - macOS regression coverage for the m-18 TASK-317.\* hygiene cluster. Extends the test surface so the macOS-platform code paths that ship today (HFS+ supported, `system_profiler` `bsd_name` partition-suffix handling, `sysinfo-modelnum-mismatch` diagnostic framework, unsupported-cascade suppression, doctor section ordering and visibility, JSON envelope shape, TOML round-trip) all have explicit pinned assertions. + + Foundation: `DevicePersona` now carries an optional `platformDeviceInfoDarwin?: PlatformDeviceInfo[] | null` field, and `ipodMacosPlatformInfo(opts)` in `@podkit/device-testing/personas/builders` synthesises canonical macOS-shape records. Populated on `ipodMini2gPink` (FAT32) and `ipodNano4gHfsplus` (HFS+) as representative fixtures. + +- [`2a644af`](https://github.com/jvgomg/podkit/commit/2a644afa386dd091e8268c8db7dac906c48e44d8) Thanks [@jvgomg](https://github.com/jvgomg)! - Fixes an infinite loop in the music sync quality-upgrade path. When a track was upgraded via direct copy (no transcode) — for example when the source bitrate increased after a re-rip — `transferUpgradeToIpod` wrote the post-encode bitrate from the preparer, which is undefined for direct copies. The file was replaced on the device but the iPod-side bitrate stayed at the previous value, so the next sync detected the same upgrade again and re-fired it indefinitely. + + The fix resolves the post-upgrade bitrate as `prepared.bitrate ?? source.bitrate` so direct-copy upgrades carry the source bitrate through. The upgrade now converges on a single sync. + + Adds a `--force-sync-tags` bitrate backfill pass for pre-existing copied tracks whose iPod-side bitrate is 0 — symmetric with the existing artwork-hash baseline backfill. New users get correct bitrate tracking on first sync; existing users opt in with `--force-sync-tags`. + + Documents the upgrade-path semantics (format-upgrade gate, quality-upgrade gate, baseline write + backfill) in `documents/architecture/sync/upgrades.md`. + +- [`7517a24`](https://github.com/jvgomg/podkit/commit/7517a2444abf629f8e032faf29c938eb74b9b51b) Thanks [@jvgomg](https://github.com/jvgomg)! - `shortenIpodLabel` (the short-label helper used in `device scan` / `device list` / `device info`) now compresses decimal-ordinal generations: `iPod Video (5.5th Generation)` → `iPod Video 5.5G`. The 5.5G iPod Video previously surfaced its full upstream string in short-label cells because the shortener's regex only matched integer ordinals (`3rd`, `5th`). + +- [`80fe65a`](https://github.com/jvgomg/podkit/commit/80fe65a022c65da512f571a8abf83f9385a649e6) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix SysInfoExtended SCSI-fallback on macOS for SCSI-only iPods (mini 2G, nano 2G, iPod 5G/5.5G). `device add` and `doctor --repair sysinfo-extended` now correctly fall back from USB → SCSI when the device does not respond to vendor control transfers, instead of failing with a misleading "Could not read device identity from USB" error. + + Internal API changes in `@podkit/ipod-firmware`: + - **Changed:** `ensureSysInfoExtended(mountPoint, fp, options?)` now takes a full `UsbFingerprint` instead of the previous `{ busNumber, deviceAddress }` shape. Required so the macOS SCSI transport can locate the IOService via vendorId/productId/serialNumber. `UsbDeviceAddress` is removed. + - **Added:** `inquireFirmwareDetailed(fp, opts?)` — like `inquireFirmware` but returns `{ firmware, plan, attempts }` so callers can distinguish which transports were attempted. `inquireFirmware` is unchanged for existing consumers. + - **Added:** `EnsureSysInfoExtendedOptions` type with `readFromUsb`, `resolveModel`, `inquireOptions` fields. Replaces the previous positional `(mountPoint, fp, readFromUsb, resolveModel)` signature. + + Internal API changes in `@podkit/core`: + - **Added:** `hasCompleteUsbFingerprint(fp): fp is CompleteUsbDevice` type guard exported from `@podkit/core`. + - **Added:** `CompleteUsbDevice` type — a `UsbFingerprint` with vendorId, productId, bus, devnum guaranteed present (serialNumber optional). + - **Changed:** `resolveUsbDeviceFromPath(path)` now also returns `vendorId` and `productId`. Linux extracts from sysfs `idVendor`/`idProduct`; macOS extracts from `system_profiler` JSON. + + User-facing error messages now differentiate between transport failures: "Could not read device identity from USB and SCSI" / "...from USB" / "...from SCSI" / "...no firmware inquiry transport is available on this system" / "...returned data but it could not be parsed". + +- [`63a69d1`](https://github.com/jvgomg/podkit/commit/63a69d11160770bcc5e251c7faf14d5c8887af13) Thanks [@jvgomg](https://github.com/jvgomg)! - New `podkit doctor` check `sysinfo-modelnum-mismatch` detects when the on-disk classic SysInfo file's `ModelNumStr` disagrees with the firmware-derived identity (e.g. SysInfo manually edited, or files copied from another iPod). Offers `--repair sysinfo-modelnum-mismatch` to overwrite the on-disk file with firmware-derived data. Identified during the TERAPOD (iPod 5G with iFlash mod) inventory pass — the SysInfo claimed `MA147` (5G) while the serial said `V9M`/`A446` (5.5G). + +- [`52894c1`](https://github.com/jvgomg/podkit/commit/52894c1977bccd51a86929debfbaa7028a19dd61) Thanks [@jvgomg](https://github.com/jvgomg)! - `@podkit/test-fixtures`: expose synthetic-track generators as a library + + Adds a library entry (`src/lib.ts`) exposing `generateMiniFlac`, `generateMiniMp3`, `generateMiniM4a`, `generateMiniOggVorbis`, and `generateMiniOggOpus`. Each helper writes a single short sine-tone file in the requested codec/container with optional metadata. Integration tests that need real audio for tag round-trip coverage now import these from `@podkit/test-fixtures` rather than re-implementing the ffmpeg invocation inline. + + Each generator calls a new `requireEncoder()` guard before invoking ffmpeg. If the host's ffmpeg is missing the codec's encoder, the helper throws a clear error with platform-aware install hints. There is also an explicit `bun run --filter @podkit/test-fixtures check-ffmpeg` script that verifies the full set of required encoders against the host environment in one shot. + + The mass-storage tag writer integration test (`packages/podkit-core/src/device/mass-storage-tag-writer.integration.test.ts`) drops its inline `generateOgg`, `generateOpus`, `generateFlac`, `generateM4a`, `generateMp3` helpers and the `HAS_LIBVORBIS` skip predicate. The OGG Vorbis tests now run unconditionally — they fail loudly with an install hint when libvorbis is absent rather than skipping silently. + + Developer docs (`docs/developers/development.md`) updated to point macOS contributors at the `homebrew-ffmpeg/ffmpeg` tap for full encoder coverage. + +- [`cdebfb3`](https://github.com/jvgomg/podkit/commit/cdebfb3512f347356bc661722d2236b359776372) Thanks [@jvgomg](https://github.com/jvgomg)! - Extend the podkit udev rule to grant Apple-vendor USB device access (`/dev/bus/usb//`) in addition to the existing SCSI generic (`/dev/sg*`) coverage. Linux libusb-based firmware inquiry now works without sudo from SSH sessions, headless boxes, Docker containers, and CI — the SSH-session permission gap previously closed only the SCSI half. `podkit doctor --repair udev-rule` installs the extended rule and cleans up any legacy filename from previous installs. + +- [`f61a83b`](https://github.com/jvgomg/podkit/commit/f61a83b3a2d13612730f174759fd3b86edd42e82) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix `podkit device scan` reporting phantom "Unknown iPod (USB only)" entries for non-iPod USB peripherals (mice, hubs, Thunderbolt docks, Ethernet adapters, USB drives). Each phantom suggested `podkit device init` — a destructive operation that could mutate an unrelated device. + + Root cause was architectural: a single `discoverUsbIpods()` function mixed three concerns — USB enumeration, iPod-domain enrichment, and the function name's implied filter (which it didn't actually do). Refactored into clean layers: + - **`@podkit/core` — pure USB enumeration.** `enumerateUsb()` returns `EnumeratedUsbDevice[]` with vendor/product/serial/bus/devnum/diskIdentifier ONLY. No iPod-domain knowledge. + - **`@podkit/devices-ipod` — iPod classifier.** `classifyAsIpod(dev)` returns `IpodClassification | null` (matches Apple-vendor with iPod or iOS PIDs). + - **`@podkit/devices-mass-storage` — mass-storage classifier.** `classifyAsMassStorage(dev)` returns `MassStorageClassification | null` (matches `USB_PRESET_HINTS` entries like Echo Mini). + - **`@podkit/core` composer.** `classifyUsbDevices()` runs both classifiers and returns recognized devices as a tagged union; drops unknown peripherals. + - **CLI `device scan`** now calls `enumerateUsb()` → `classifyUsbDevices()` → renders by `kind`. No domain logic in the command layer. + + Added `kind: 'mass-storage'` rendering branch so mass-storage DAPs are no longer mis-labeled as "Unknown iPod". + + Removed `discoverUsbIpods` and the leaky `UsbDiscoveredDevice` type (which previously carried iPod-domain fields). Adding a new mass-storage device now means adding one entry to `USB_PRESET_HINTS` — no `@podkit/core` change required. Adding a new iPod generation means updating `@podkit/devices-ipod` tables — no other package changes. + + Also split `usb-discovery.ts` into `usb-enumeration.ts` (bus walk) + `usb-path-resolution.ts` (mount-path → fingerprint resolver) since those two concerns were unrelated. + +- [`6000868`](https://github.com/jvgomg/podkit/commit/6000868830d9437a6fff3c1a77adb254d9579fe7) Thanks [@jvgomg](https://github.com/jvgomg)! - Fix `doctor --repair sysinfo-extended` showing unhelpful "Could not read device identity from USB" with no detail. The native USB binding now throws descriptive errors (e.g. "USB control transfer failed (bus 3, device 4)") instead of returning null silently. Also fix all doctor repair intro messages — they incorrectly said "Repairing X for N tracks" even for non-track operations like SysInfoExtended and orphan cleanup. Intro messages now use each repair's own description. + +- Updated dependencies [[`bb2e637`](https://github.com/jvgomg/podkit/commit/bb2e6374151605d11baf052c452f10a842e5353e), [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af), [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b), [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6), [`01ecedd`](https://github.com/jvgomg/podkit/commit/01ecedde623ff99e94c5cbda75ff9f9c9ecef632), [`a78e5fe`](https://github.com/jvgomg/podkit/commit/a78e5fee4e47293c1935395bb157cb6574782625), [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce), [`eed4126`](https://github.com/jvgomg/podkit/commit/eed4126fe91ff64f00d74e8a2aaaae38ca6d786b), [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4), [`22dddf4`](https://github.com/jvgomg/podkit/commit/22dddf4803f4cfd7b004d80dffd83878a68b10f2), [`1ec30ac`](https://github.com/jvgomg/podkit/commit/1ec30acca1109178012db3913a60967a2087fb5b), [`80fe65a`](https://github.com/jvgomg/podkit/commit/80fe65a022c65da512f571a8abf83f9385a649e6), [`c5cba69`](https://github.com/jvgomg/podkit/commit/c5cba6998283663b42659f02b17b194ab256c137), [`f61a83b`](https://github.com/jvgomg/podkit/commit/f61a83b3a2d13612730f174759fd3b86edd42e82), [`6000868`](https://github.com/jvgomg/podkit/commit/6000868830d9437a6fff3c1a77adb254d9579fe7), [`4598f8f`](https://github.com/jvgomg/podkit/commit/4598f8f3347cf40b94fdf1585215e5b0f54d9cf6), [`e825ee1`](https://github.com/jvgomg/podkit/commit/e825ee1dd4933ecbfd070dda27f96f43056f0baf)]: + - @podkit/ipod-firmware@0.1.0 + - @podkit/device-types@0.1.0 + - @podkit/devices-mass-storage@0.1.0 + - @podkit/devices-ipod@0.1.0 + - @podkit/libgpod-node@0.2.0 + ## 0.6.0 ### Minor Changes diff --git a/packages/podkit-core/package.json b/packages/podkit-core/package.json index b67943dc..75955592 100644 --- a/packages/podkit-core/package.json +++ b/packages/podkit-core/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/core", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/podkit-daemon/CHANGELOG.md b/packages/podkit-daemon/CHANGELOG.md index 40f26030..c62f7c95 100644 --- a/packages/podkit-daemon/CHANGELOG.md +++ b/packages/podkit-daemon/CHANGELOG.md @@ -1,5 +1,95 @@ # @podkit/daemon +## 0.3.0 + +### Minor Changes + +- [`484fb0e`](https://github.com/jvgomg/podkit/commit/484fb0ea63eea297f19217d1acb96163a6754b05) Thanks [@jvgomg](https://github.com/jvgomg)! - Cross-process sync coordination: per-device lock, transcode owner-liveness, phantom auto-prune. + + **Per-device sync lock.** `podkit sync` now acquires a per-device PID-file lock at `.podkit/sync.lock` (mass-storage) or `iPod_Control/.podkit-sync.lock` (iPod) immediately after opening the device. A second concurrent `podkit sync` against the same device exits with the new `LOCK_HELD` code (exit code **4**) and a message naming the holding PID: + + ``` + Error: Another podkit process is already syncing /Volumes/TERAPOD (pid 12345). Wait for it to finish or kill it. + ``` + + Crash-safe: the lock file is unlinked in `finally`; if a process is SIGKILLed mid-sync, the next attempt detects the dead PID via liveness probe and takes over cleanly. `podkit sync --dry-run` does **not** take the lock (read-only by design). The daemon (`@podkit/daemon`) detects `LOCK_HELD` from the CLI subprocess and skips that cycle (no retry-spin). Read commands (`device scan`, `device info`, `device music`) are unaffected — writes only. + + **Transcode-tmp owner-liveness.** Each `podkit-transcode-/` scratch directory now writes a small `.owner` file at creation. The pre-sync sweep walker reaps dirs whose owner is dead or whose `.owner` is missing; live owners are left alone. This replaces the previous session-start-time floor, which missed the most common interruption case for the long-running daemon: its own prior cycle. + + **Phantom manifest auto-prune.** When the pre-sync sweep detects manifest rows whose backing audio file has vanished (mass-storage devices), it now prunes them atomically as part of the sweep. Previously the sweep emitted an advisory warning recommending `podkit doctor --repair orphan-files`; the advisory now fires only if the auto-prune itself fails. `doctor --repair orphan-files` remains as a backstop and is unchanged. + + **Filesystem support.** The shared liveness primitive uses only `O_CREAT|O_EXCL`, `unlink`, `rename`, `readFile`, `writeFile`, and `process.kill(pid, 0)` — stable across exFAT, FAT32, HFS+, APFS, ext4, and NTFS. We deliberately avoided `flock(2)` because its semantics on FAT-family filesystems (common for iPods) are platform-dependent. + + **Internal API additions** (`@podkit/core`): `acquireLock`, `LockHandle`, `LockHeldError`, `LockContestedError`, `getOwnIdentity`, `readOwnership`, `writeOwnership`, `isAlive`, `PidFileEntry` — all from a new `lib/pid-file.ts` primitive. `DeviceAdapter` gains an optional `prunePhantomManifest?(paths)` method (implemented on mass-storage, intentionally omitted on iPod). + + **Removed internals:** `SESSION_START_MS` constant and `sessionStartMsOverride` plumbing in `pre-sync-sweep.ts` are gone — replaced wholesale by the PID-file liveness probe. + + See `documents/architecture/sync/planning.md` §6 for the cross-process coordination design. + +- [#58](https://github.com/jvgomg/podkit/pull/58) [`efa14c6`](https://github.com/jvgomg/podkit/commit/efa14c623e7bda81066bd77142cddb28e4de615d) Thanks [@jvgomg](https://github.com/jvgomg)! - Add mass-storage device support for non-iPod portable music players. + + **Supported device types:** Echo Mini, Rockbox, and generic mass-storage DAPs. iPod support is unchanged. + + **New in CLI (`podkit`):** + - `podkit device add --type ` registers mass-storage devices by type and mount path + - `podkit device info/music/video` work with mass-storage devices via `DeviceAdapter` interface + - `podkit device scan` shows configured path-based devices alongside auto-detected iPods + - `podkit sync` routes to the correct adapter (iPod or mass-storage) based on device config + - Video sync now uses capabilities-based gating instead of iPod-only checks + - Safety gates on `device init/reset/clear` (iPod-only commands) for mass-storage devices + - Mount and eject commands show device-appropriate messaging + - Config validation rejects capability overrides on iPod devices (capabilities are auto-detected from generation) + - Shared `openDevice()` function eliminates duplicated device-opening logic across commands + + **New in core (`@podkit/core`):** + - `DeviceAdapter` interface — generic abstraction over device databases (iPod, mass-storage) + - `MassStorageAdapter` — filesystem-based track management with `.podkit/state.json` manifest + - `IpodDeviceAdapter` — thin wrapper making `IpodDatabase` implement `DeviceAdapter` + - Device capability presets for Echo Mini, Rockbox, and generic devices + - `resolveDeviceCapabilities()` merges preset defaults with user config overrides + - `DeviceTrack` type used throughout sync engine (replaces `IPodTrack` casts in execution paths) + - Configurable content path prefixes (`musicDir`, `moviesDir`, `tvShowsDir`) with device-type defaults + - Device presets include default content paths (Echo Mini: root for music; generic/Rockbox: `Music/`, `Video/Movies/`, `Video/Shows/`) + - Manifest v2 stores active content paths; files automatically moved when prefixes change + - Root path support (`/`, `.`, or empty string all normalize to device root) + - Content path duplicate validation (no two content types can share the same prefix) + - Video scanning support for mass-storage devices (.m4v, .mp4, .mov, .avi, .mkv) + + **New in daemon (`@podkit/daemon`):** + - Mass-storage device polling via `PODKIT_MASS_STORAGE_PATHS` env var (colon/comma separated) + - Second `DevicePoller` + `SyncOrchestrator` pair for mass-storage devices + - No-op mount/eject runners (mass-storage devices are externally managed) + - Graceful shutdown handles both iPod and mass-storage sync pipelines + + **Configuration:** + + ```toml + [devices.echo] + type = "echo-mini" + path = "/Volumes/ECHO" + + # Optional capability overrides (mass-storage only) + artworkMaxResolution = 800 + supportedAudioCodecs = ["aac", "mp3", "flac"] + + # Optional content path overrides (mass-storage only) + musicDir = "/" # Place music at device root + moviesDir = "Films" # Custom movies directory + tvShowsDir = "TV Shows" # Custom TV shows directory + ``` + + **Environment variables for content paths:** + - `PODKIT_MUSIC_DIR` — global default music directory + - `PODKIT_MOVIES_DIR` — global default movies directory + - `PODKIT_TV_SHOWS_DIR` — global default TV shows directory + +- [`455e115`](https://github.com/jvgomg/podkit/commit/455e115d5f724411f970ed49dda2cca57c7aff2f) Thanks [@jvgomg](https://github.com/jvgomg)! - Support multiple iPods plugged in simultaneously. Each device gets a unique mount point and devices appearing during a sync are queued and synced sequentially after the current sync completes. + +### Patch Changes + +- Updated dependencies [[`0f3e4dd`](https://github.com/jvgomg/podkit/commit/0f3e4ddae134228b5e874b21db33f74547867b6c), [`036b107`](https://github.com/jvgomg/podkit/commit/036b1077748253385b6f4ff873a7cdb52c54b004), [`621b10a`](https://github.com/jvgomg/podkit/commit/621b10abbec3a8e369da9620733210fef4b76f99), [`89ff40c`](https://github.com/jvgomg/podkit/commit/89ff40c2adedd9fec38ae5ad0eb89b75525642f2), [`c5c0236`](https://github.com/jvgomg/podkit/commit/c5c0236c232cc3fa086fd3937b0e2fbe0f326185), [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af), [`513173d`](https://github.com/jvgomg/podkit/commit/513173d1832bf9ca2894214e97d9d65cf02c52a5), [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b), [`0cc39d3`](https://github.com/jvgomg/podkit/commit/0cc39d3c62343591127d5c79deed2478f8dc4f60), [`22dddf4`](https://github.com/jvgomg/podkit/commit/22dddf4803f4cfd7b004d80dffd83878a68b10f2), [`484fb0e`](https://github.com/jvgomg/podkit/commit/484fb0ea63eea297f19217d1acb96163a6754b05), [`348f2c5`](https://github.com/jvgomg/podkit/commit/348f2c53cec06598903b5cf128663d5121c46865), [`7534c2f`](https://github.com/jvgomg/podkit/commit/7534c2f19d81087413af8abbf764fe20cef61384), [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6), [`6747667`](https://github.com/jvgomg/podkit/commit/6747667049cd793fdb13e3d1bc1092651f8e969c), [`8bc3126`](https://github.com/jvgomg/podkit/commit/8bc3126ec415aa836b746ec921b6738abdd9e538), [`87cb87a`](https://github.com/jvgomg/podkit/commit/87cb87aef59ad366b4c6c2b4c22f897f0b84a54a), [`01ecedd`](https://github.com/jvgomg/podkit/commit/01ecedde623ff99e94c5cbda75ff9f9c9ecef632), [`667d66b`](https://github.com/jvgomg/podkit/commit/667d66b90e0979aaff381968358f2cfc78c8e581), [`03f1046`](https://github.com/jvgomg/podkit/commit/03f1046b70898b0282d0c96927bca60ee0d55eeb), [`78b0c71`](https://github.com/jvgomg/podkit/commit/78b0c71b9866306aecbb96f2a0e372a86564f2fc), [`14d83e5`](https://github.com/jvgomg/podkit/commit/14d83e5e59eb0a8a801850de775f9fdb4c0e7aa9), [`a78e5fe`](https://github.com/jvgomg/podkit/commit/a78e5fee4e47293c1935395bb157cb6574782625), [`e0f65f4`](https://github.com/jvgomg/podkit/commit/e0f65f4b0cf4fce28138849b7a85f2c3a7c1a613), [`785ad57`](https://github.com/jvgomg/podkit/commit/785ad57af6627059fe3a6d7e1fef475e82c34764), [`4efa15c`](https://github.com/jvgomg/podkit/commit/4efa15c7e42874e9dd88ef2731230d5314d83f20), [`3db3d88`](https://github.com/jvgomg/podkit/commit/3db3d887ae2cd19d01ba2c1f00b8682e783fac84), [`7ebb7c5`](https://github.com/jvgomg/podkit/commit/7ebb7c5c0e1c7c3d549196347029d9ce660fcb8b), [`34e8bf2`](https://github.com/jvgomg/podkit/commit/34e8bf2341111df1e8f85361b8047eed9f31665a), [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce), [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4), [`09c4acd`](https://github.com/jvgomg/podkit/commit/09c4acdec349f200a649b2db15fe05345e380a7b), [`151152a`](https://github.com/jvgomg/podkit/commit/151152ae835529730b3235a780550ec35ad685e2), [`94c85d2`](https://github.com/jvgomg/podkit/commit/94c85d2a9d6c85875432a0ebecab540a9ebd67d7), [`efa14c6`](https://github.com/jvgomg/podkit/commit/efa14c623e7bda81066bd77142cddb28e4de615d), [`208e482`](https://github.com/jvgomg/podkit/commit/208e482db9730064a25e53e03121bdcfcbea6341), [`2a644af`](https://github.com/jvgomg/podkit/commit/2a644afa386dd091e8268c8db7dac906c48e44d8), [`f5d0082`](https://github.com/jvgomg/podkit/commit/f5d00829f3b1a80453bdc4f7e6599566f7f02bb3), [`4ee5e2b`](https://github.com/jvgomg/podkit/commit/4ee5e2be470a93a54c2d54bc0aab257d7b92babe), [`303c35a`](https://github.com/jvgomg/podkit/commit/303c35aea57c0f35f64481e12e5cb9298e9a5631), [`bb96778`](https://github.com/jvgomg/podkit/commit/bb96778dde9063267188b2b83535ec279cd5c550), [`7517a24`](https://github.com/jvgomg/podkit/commit/7517a2444abf629f8e032faf29c938eb74b9b51b), [`275c972`](https://github.com/jvgomg/podkit/commit/275c97295462547037e2c911c139654eb50d4af7), [`cac7fc1`](https://github.com/jvgomg/podkit/commit/cac7fc123861e97b10d31c83728a1e3f0431934e), [`f72fa01`](https://github.com/jvgomg/podkit/commit/f72fa0170872fc0a6e5719b4509abae24e6414cd), [`7bf7127`](https://github.com/jvgomg/podkit/commit/7bf7127d3141ce4b91138e3284b18aa5e8ea5984), [`c9c268e`](https://github.com/jvgomg/podkit/commit/c9c268ea4b25b39543e5c53a1928e72b4c31e0c8), [`1ec30ac`](https://github.com/jvgomg/podkit/commit/1ec30acca1109178012db3913a60967a2087fb5b), [`80fe65a`](https://github.com/jvgomg/podkit/commit/80fe65a022c65da512f571a8abf83f9385a649e6), [`63a69d1`](https://github.com/jvgomg/podkit/commit/63a69d11160770bcc5e251c7faf14d5c8887af13), [`52894c1`](https://github.com/jvgomg/podkit/commit/52894c1977bccd51a86929debfbaa7028a19dd61), [`c5cba69`](https://github.com/jvgomg/podkit/commit/c5cba6998283663b42659f02b17b194ab256c137), [`1c3ebc3`](https://github.com/jvgomg/podkit/commit/1c3ebc381276accdb8361f50454b90c75f2391df), [`cdebfb3`](https://github.com/jvgomg/podkit/commit/cdebfb3512f347356bc661722d2236b359776372), [`ec8dc85`](https://github.com/jvgomg/podkit/commit/ec8dc8549447b0178a8746b8cda2b8b7908b9d04), [`f61a83b`](https://github.com/jvgomg/podkit/commit/f61a83b3a2d13612730f174759fd3b86edd42e82), [`6000868`](https://github.com/jvgomg/podkit/commit/6000868830d9437a6fff3c1a77adb254d9579fe7), [`e825ee1`](https://github.com/jvgomg/podkit/commit/e825ee1dd4933ecbfd070dda27f96f43056f0baf)]: + - @podkit/core@0.7.0 + ## 0.2.2 ### Patch Changes diff --git a/packages/podkit-daemon/package.json b/packages/podkit-daemon/package.json index 6d9e1585..5723955f 100644 --- a/packages/podkit-daemon/package.json +++ b/packages/podkit-daemon/package.json @@ -1,7 +1,7 @@ { "name": "@podkit/daemon", "private": true, - "version": "0.2.2", + "version": "0.3.0", "description": "Daemon for auto-detecting iPods and syncing via the podkit CLI", "type": "module", "main": "./dist/main.js", diff --git a/packages/podkit-docker/CHANGELOG.md b/packages/podkit-docker/CHANGELOG.md index ab051353..57bb5a3b 100644 --- a/packages/podkit-docker/CHANGELOG.md +++ b/packages/podkit-docker/CHANGELOG.md @@ -1,5 +1,13 @@ # @podkit/docker +## 0.2.4 + +### Patch Changes + +- Updated dependencies [[`0f3e4dd`](https://github.com/jvgomg/podkit/commit/0f3e4ddae134228b5e874b21db33f74547867b6c), [`036b107`](https://github.com/jvgomg/podkit/commit/036b1077748253385b6f4ff873a7cdb52c54b004), [`89ff40c`](https://github.com/jvgomg/podkit/commit/89ff40c2adedd9fec38ae5ad0eb89b75525642f2), [`c5c0236`](https://github.com/jvgomg/podkit/commit/c5c0236c232cc3fa086fd3937b0e2fbe0f326185), [`bb2e637`](https://github.com/jvgomg/podkit/commit/bb2e6374151605d11baf052c452f10a842e5353e), [`0ef210b`](https://github.com/jvgomg/podkit/commit/0ef210be6e5fc38203e5501d33cc1bb978ecc0c6), [`ae995ef`](https://github.com/jvgomg/podkit/commit/ae995ef99174f7381f4eeaeb79cee4e77ddc3136), [`8c6dc1a`](https://github.com/jvgomg/podkit/commit/8c6dc1a3d355efe6542a7f00f8fa05da3225bb42), [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af), [`56c7ec3`](https://github.com/jvgomg/podkit/commit/56c7ec36fb00b6996beffdce76eb17a23211c628), [`513173d`](https://github.com/jvgomg/podkit/commit/513173d1832bf9ca2894214e97d9d65cf02c52a5), [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b), [`0cc39d3`](https://github.com/jvgomg/podkit/commit/0cc39d3c62343591127d5c79deed2478f8dc4f60), [`484fb0e`](https://github.com/jvgomg/podkit/commit/484fb0ea63eea297f19217d1acb96163a6754b05), [`59726b1`](https://github.com/jvgomg/podkit/commit/59726b1f63e54afb02cef85d6f58755677b881ee), [`348f2c5`](https://github.com/jvgomg/podkit/commit/348f2c53cec06598903b5cf128663d5121c46865), [`6db8fb0`](https://github.com/jvgomg/podkit/commit/6db8fb0c587cfd9e350516631b2030f165c542ce), [`7534c2f`](https://github.com/jvgomg/podkit/commit/7534c2f19d81087413af8abbf764fe20cef61384), [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6), [`8bc3126`](https://github.com/jvgomg/podkit/commit/8bc3126ec415aa836b746ec921b6738abdd9e538), [`7d7a429`](https://github.com/jvgomg/podkit/commit/7d7a4294b81b42f03143edbad62e746633f0e4d9), [`eb0b215`](https://github.com/jvgomg/podkit/commit/eb0b21527a09c71542f6e860876fdf0e763e3f41), [`8d017e8`](https://github.com/jvgomg/podkit/commit/8d017e8ede48c98b9a1d1c627b882689c33da61e), [`6747667`](https://github.com/jvgomg/podkit/commit/6747667049cd793fdb13e3d1bc1092651f8e969c), [`14458fd`](https://github.com/jvgomg/podkit/commit/14458fd3454629e33c9f4894e899e7f3e47e5cf5), [`01ecedd`](https://github.com/jvgomg/podkit/commit/01ecedde623ff99e94c5cbda75ff9f9c9ecef632), [`667d66b`](https://github.com/jvgomg/podkit/commit/667d66b90e0979aaff381968358f2cfc78c8e581), [`03f1046`](https://github.com/jvgomg/podkit/commit/03f1046b70898b0282d0c96927bca60ee0d55eeb), [`78b0c71`](https://github.com/jvgomg/podkit/commit/78b0c71b9866306aecbb96f2a0e372a86564f2fc), [`14d83e5`](https://github.com/jvgomg/podkit/commit/14d83e5e59eb0a8a801850de775f9fdb4c0e7aa9), [`a78e5fe`](https://github.com/jvgomg/podkit/commit/a78e5fee4e47293c1935395bb157cb6574782625), [`e0f65f4`](https://github.com/jvgomg/podkit/commit/e0f65f4b0cf4fce28138849b7a85f2c3a7c1a613), [`5b0279c`](https://github.com/jvgomg/podkit/commit/5b0279cc620ed5101351494dc1ea4a709791d0db), [`785ad57`](https://github.com/jvgomg/podkit/commit/785ad57af6627059fe3a6d7e1fef475e82c34764), [`4efa15c`](https://github.com/jvgomg/podkit/commit/4efa15c7e42874e9dd88ef2731230d5314d83f20), [`3db3d88`](https://github.com/jvgomg/podkit/commit/3db3d887ae2cd19d01ba2c1f00b8682e783fac84), [`7ebb7c5`](https://github.com/jvgomg/podkit/commit/7ebb7c5c0e1c7c3d549196347029d9ce660fcb8b), [`1caab19`](https://github.com/jvgomg/podkit/commit/1caab1991d43739aaba3d9ae2e4a5dd6575f331a), [`3fe7853`](https://github.com/jvgomg/podkit/commit/3fe785330f8b92c21159ae253456942a92e7c8e2), [`26733cc`](https://github.com/jvgomg/podkit/commit/26733cc77fd56681387b29e4241ad05e4d1fd348), [`e58ae80`](https://github.com/jvgomg/podkit/commit/e58ae806a494e3f526a828d4b72dab558ae4b121), [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce), [`eed4126`](https://github.com/jvgomg/podkit/commit/eed4126fe91ff64f00d74e8a2aaaae38ca6d786b), [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4), [`09c4acd`](https://github.com/jvgomg/podkit/commit/09c4acdec349f200a649b2db15fe05345e380a7b), [`151152a`](https://github.com/jvgomg/podkit/commit/151152ae835529730b3235a780550ec35ad685e2), [`94c85d2`](https://github.com/jvgomg/podkit/commit/94c85d2a9d6c85875432a0ebecab540a9ebd67d7), [`efa14c6`](https://github.com/jvgomg/podkit/commit/efa14c623e7bda81066bd77142cddb28e4de615d), [`455e115`](https://github.com/jvgomg/podkit/commit/455e115d5f724411f970ed49dda2cca57c7aff2f), [`34ad4d3`](https://github.com/jvgomg/podkit/commit/34ad4d39104600f1363bd434518b10b3399a652c), [`2a644af`](https://github.com/jvgomg/podkit/commit/2a644afa386dd091e8268c8db7dac906c48e44d8), [`f5d0082`](https://github.com/jvgomg/podkit/commit/f5d00829f3b1a80453bdc4f7e6599566f7f02bb3), [`4ee5e2b`](https://github.com/jvgomg/podkit/commit/4ee5e2be470a93a54c2d54bc0aab257d7b92babe), [`275c972`](https://github.com/jvgomg/podkit/commit/275c97295462547037e2c911c139654eb50d4af7), [`cac7fc1`](https://github.com/jvgomg/podkit/commit/cac7fc123861e97b10d31c83728a1e3f0431934e), [`f72fa01`](https://github.com/jvgomg/podkit/commit/f72fa0170872fc0a6e5719b4509abae24e6414cd), [`7bf7127`](https://github.com/jvgomg/podkit/commit/7bf7127d3141ce4b91138e3284b18aa5e8ea5984), [`1ec30ac`](https://github.com/jvgomg/podkit/commit/1ec30acca1109178012db3913a60967a2087fb5b), [`80fe65a`](https://github.com/jvgomg/podkit/commit/80fe65a022c65da512f571a8abf83f9385a649e6), [`63a69d1`](https://github.com/jvgomg/podkit/commit/63a69d11160770bcc5e251c7faf14d5c8887af13), [`c5cba69`](https://github.com/jvgomg/podkit/commit/c5cba6998283663b42659f02b17b194ab256c137), [`1c3ebc3`](https://github.com/jvgomg/podkit/commit/1c3ebc381276accdb8361f50454b90c75f2391df), [`17eac11`](https://github.com/jvgomg/podkit/commit/17eac114719f93cef40beb58381e534a28ebc35f), [`cdebfb3`](https://github.com/jvgomg/podkit/commit/cdebfb3512f347356bc661722d2236b359776372), [`ec8dc85`](https://github.com/jvgomg/podkit/commit/ec8dc8549447b0178a8746b8cda2b8b7908b9d04), [`f61a83b`](https://github.com/jvgomg/podkit/commit/f61a83b3a2d13612730f174759fd3b86edd42e82), [`6000868`](https://github.com/jvgomg/podkit/commit/6000868830d9437a6fff3c1a77adb254d9579fe7), [`4598f8f`](https://github.com/jvgomg/podkit/commit/4598f8f3347cf40b94fdf1585215e5b0f54d9cf6), [`e825ee1`](https://github.com/jvgomg/podkit/commit/e825ee1dd4933ecbfd070dda27f96f43056f0baf), [`72fd2c8`](https://github.com/jvgomg/podkit/commit/72fd2c8521cfa8f8d7102a6d1707d8282f67e6d2)]: + - podkit@0.7.0 + - @podkit/daemon@0.3.0 + ## 0.2.3 ### Patch Changes diff --git a/packages/podkit-docker/package.json b/packages/podkit-docker/package.json index e56f7466..8748f877 100644 --- a/packages/podkit-docker/package.json +++ b/packages/podkit-docker/package.json @@ -1,7 +1,7 @@ { "name": "@podkit/docker", "private": true, - "version": "0.2.3", + "version": "0.2.4", "description": "Docker image for podkit — syncs music collections to iPod devices", "dependencies": { "@podkit/daemon": "workspace:*", diff --git a/test-packages/device-testing-daemon/CHANGELOG.md b/test-packages/device-testing-daemon/CHANGELOG.md new file mode 100644 index 00000000..40405c8b --- /dev/null +++ b/test-packages/device-testing-daemon/CHANGELOG.md @@ -0,0 +1,8 @@ +# @podkit/device-testing-daemon + +## 0.0.2 + +### Patch Changes + +- Updated dependencies [[`151152a`](https://github.com/jvgomg/podkit/commit/151152ae835529730b3235a780550ec35ad685e2)]: + - @podkit/device-testing@0.0.2 diff --git a/test-packages/device-testing-daemon/package.json b/test-packages/device-testing-daemon/package.json index 890ac56e..ac22002f 100644 --- a/test-packages/device-testing-daemon/package.json +++ b/test-packages/device-testing-daemon/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/device-testing-daemon", - "version": "0.0.1", + "version": "0.0.2", "private": true, "description": "FunctionFS userspace daemon: synthesises iPod-shaped USB devices on Linux dummy_hcd for VM tests (ADR-016). Compiled to a single self-contained binary via `bun build --compile`; consumed by @podkit/device-testing's lima-test-vm runner.", "type": "module", diff --git a/test-packages/device-testing/CHANGELOG.md b/test-packages/device-testing/CHANGELOG.md new file mode 100644 index 00000000..c28bb058 --- /dev/null +++ b/test-packages/device-testing/CHANGELOG.md @@ -0,0 +1,13 @@ +# @podkit/device-testing + +## 0.0.2 + +### Patch Changes + +- [`151152a`](https://github.com/jvgomg/podkit/commit/151152ae835529730b3235a780550ec35ad685e2) Thanks [@jvgomg](https://github.com/jvgomg)! - macOS regression coverage for the m-18 TASK-317.\* hygiene cluster. Extends the test surface so the macOS-platform code paths that ship today (HFS+ supported, `system_profiler` `bsd_name` partition-suffix handling, `sysinfo-modelnum-mismatch` diagnostic framework, unsupported-cascade suppression, doctor section ordering and visibility, JSON envelope shape, TOML round-trip) all have explicit pinned assertions. + + Foundation: `DevicePersona` now carries an optional `platformDeviceInfoDarwin?: PlatformDeviceInfo[] | null` field, and `ipodMacosPlatformInfo(opts)` in `@podkit/device-testing/personas/builders` synthesises canonical macOS-shape records. Populated on `ipodMini2gPink` (FAT32) and `ipodNano4gHfsplus` (HFS+) as representative fixtures. + +- Updated dependencies [[`0f3e4dd`](https://github.com/jvgomg/podkit/commit/0f3e4ddae134228b5e874b21db33f74547867b6c), [`036b107`](https://github.com/jvgomg/podkit/commit/036b1077748253385b6f4ff873a7cdb52c54b004), [`621b10a`](https://github.com/jvgomg/podkit/commit/621b10abbec3a8e369da9620733210fef4b76f99), [`89ff40c`](https://github.com/jvgomg/podkit/commit/89ff40c2adedd9fec38ae5ad0eb89b75525642f2), [`c5c0236`](https://github.com/jvgomg/podkit/commit/c5c0236c232cc3fa086fd3937b0e2fbe0f326185), [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af), [`513173d`](https://github.com/jvgomg/podkit/commit/513173d1832bf9ca2894214e97d9d65cf02c52a5), [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b), [`0cc39d3`](https://github.com/jvgomg/podkit/commit/0cc39d3c62343591127d5c79deed2478f8dc4f60), [`22dddf4`](https://github.com/jvgomg/podkit/commit/22dddf4803f4cfd7b004d80dffd83878a68b10f2), [`484fb0e`](https://github.com/jvgomg/podkit/commit/484fb0ea63eea297f19217d1acb96163a6754b05), [`348f2c5`](https://github.com/jvgomg/podkit/commit/348f2c53cec06598903b5cf128663d5121c46865), [`7534c2f`](https://github.com/jvgomg/podkit/commit/7534c2f19d81087413af8abbf764fe20cef61384), [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6), [`6747667`](https://github.com/jvgomg/podkit/commit/6747667049cd793fdb13e3d1bc1092651f8e969c), [`8bc3126`](https://github.com/jvgomg/podkit/commit/8bc3126ec415aa836b746ec921b6738abdd9e538), [`87cb87a`](https://github.com/jvgomg/podkit/commit/87cb87aef59ad366b4c6c2b4c22f897f0b84a54a), [`01ecedd`](https://github.com/jvgomg/podkit/commit/01ecedde623ff99e94c5cbda75ff9f9c9ecef632), [`667d66b`](https://github.com/jvgomg/podkit/commit/667d66b90e0979aaff381968358f2cfc78c8e581), [`03f1046`](https://github.com/jvgomg/podkit/commit/03f1046b70898b0282d0c96927bca60ee0d55eeb), [`78b0c71`](https://github.com/jvgomg/podkit/commit/78b0c71b9866306aecbb96f2a0e372a86564f2fc), [`14d83e5`](https://github.com/jvgomg/podkit/commit/14d83e5e59eb0a8a801850de775f9fdb4c0e7aa9), [`a78e5fe`](https://github.com/jvgomg/podkit/commit/a78e5fee4e47293c1935395bb157cb6574782625), [`e0f65f4`](https://github.com/jvgomg/podkit/commit/e0f65f4b0cf4fce28138849b7a85f2c3a7c1a613), [`785ad57`](https://github.com/jvgomg/podkit/commit/785ad57af6627059fe3a6d7e1fef475e82c34764), [`4efa15c`](https://github.com/jvgomg/podkit/commit/4efa15c7e42874e9dd88ef2731230d5314d83f20), [`3db3d88`](https://github.com/jvgomg/podkit/commit/3db3d887ae2cd19d01ba2c1f00b8682e783fac84), [`7ebb7c5`](https://github.com/jvgomg/podkit/commit/7ebb7c5c0e1c7c3d549196347029d9ce660fcb8b), [`34e8bf2`](https://github.com/jvgomg/podkit/commit/34e8bf2341111df1e8f85361b8047eed9f31665a), [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce), [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4), [`09c4acd`](https://github.com/jvgomg/podkit/commit/09c4acdec349f200a649b2db15fe05345e380a7b), [`151152a`](https://github.com/jvgomg/podkit/commit/151152ae835529730b3235a780550ec35ad685e2), [`94c85d2`](https://github.com/jvgomg/podkit/commit/94c85d2a9d6c85875432a0ebecab540a9ebd67d7), [`efa14c6`](https://github.com/jvgomg/podkit/commit/efa14c623e7bda81066bd77142cddb28e4de615d), [`208e482`](https://github.com/jvgomg/podkit/commit/208e482db9730064a25e53e03121bdcfcbea6341), [`2a644af`](https://github.com/jvgomg/podkit/commit/2a644afa386dd091e8268c8db7dac906c48e44d8), [`f5d0082`](https://github.com/jvgomg/podkit/commit/f5d00829f3b1a80453bdc4f7e6599566f7f02bb3), [`4ee5e2b`](https://github.com/jvgomg/podkit/commit/4ee5e2be470a93a54c2d54bc0aab257d7b92babe), [`303c35a`](https://github.com/jvgomg/podkit/commit/303c35aea57c0f35f64481e12e5cb9298e9a5631), [`bb96778`](https://github.com/jvgomg/podkit/commit/bb96778dde9063267188b2b83535ec279cd5c550), [`7517a24`](https://github.com/jvgomg/podkit/commit/7517a2444abf629f8e032faf29c938eb74b9b51b), [`275c972`](https://github.com/jvgomg/podkit/commit/275c97295462547037e2c911c139654eb50d4af7), [`cac7fc1`](https://github.com/jvgomg/podkit/commit/cac7fc123861e97b10d31c83728a1e3f0431934e), [`f72fa01`](https://github.com/jvgomg/podkit/commit/f72fa0170872fc0a6e5719b4509abae24e6414cd), [`7bf7127`](https://github.com/jvgomg/podkit/commit/7bf7127d3141ce4b91138e3284b18aa5e8ea5984), [`c9c268e`](https://github.com/jvgomg/podkit/commit/c9c268ea4b25b39543e5c53a1928e72b4c31e0c8), [`1ec30ac`](https://github.com/jvgomg/podkit/commit/1ec30acca1109178012db3913a60967a2087fb5b), [`80fe65a`](https://github.com/jvgomg/podkit/commit/80fe65a022c65da512f571a8abf83f9385a649e6), [`63a69d1`](https://github.com/jvgomg/podkit/commit/63a69d11160770bcc5e251c7faf14d5c8887af13), [`52894c1`](https://github.com/jvgomg/podkit/commit/52894c1977bccd51a86929debfbaa7028a19dd61), [`c5cba69`](https://github.com/jvgomg/podkit/commit/c5cba6998283663b42659f02b17b194ab256c137), [`1c3ebc3`](https://github.com/jvgomg/podkit/commit/1c3ebc381276accdb8361f50454b90c75f2391df), [`cdebfb3`](https://github.com/jvgomg/podkit/commit/cdebfb3512f347356bc661722d2236b359776372), [`ec8dc85`](https://github.com/jvgomg/podkit/commit/ec8dc8549447b0178a8746b8cda2b8b7908b9d04), [`f61a83b`](https://github.com/jvgomg/podkit/commit/f61a83b3a2d13612730f174759fd3b86edd42e82), [`6000868`](https://github.com/jvgomg/podkit/commit/6000868830d9437a6fff3c1a77adb254d9579fe7), [`e825ee1`](https://github.com/jvgomg/podkit/commit/e825ee1dd4933ecbfd070dda27f96f43056f0baf)]: + - @podkit/core@0.7.0 + - @podkit/device-types@0.1.0 diff --git a/test-packages/device-testing/package.json b/test-packages/device-testing/package.json index a5f680bd..f26d4457 100644 --- a/test-packages/device-testing/package.json +++ b/test-packages/device-testing/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/device-testing", - "version": "0.0.1", + "version": "0.0.2", "description": "Device persona + system-state fixtures and test runtime harness for podkit's VM integration tests", "type": "module", "main": "./dist/index.js", diff --git a/test-packages/e2e-shared/CHANGELOG.md b/test-packages/e2e-shared/CHANGELOG.md new file mode 100644 index 00000000..21b25027 --- /dev/null +++ b/test-packages/e2e-shared/CHANGELOG.md @@ -0,0 +1,9 @@ +# @podkit/e2e-shared + +## 0.0.1 + +### Patch Changes + +- Updated dependencies [[`52894c1`](https://github.com/jvgomg/podkit/commit/52894c1977bccd51a86929debfbaa7028a19dd61)]: + - @podkit/test-fixtures@0.1.0 + - @podkit/gpod-testing@0.0.0 diff --git a/test-packages/e2e-shared/package.json b/test-packages/e2e-shared/package.json index 132759cf..795c6321 100644 --- a/test-packages/e2e-shared/package.json +++ b/test-packages/e2e-shared/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/e2e-shared", - "version": "0.0.0", + "version": "0.0.1", "description": "Cross-cutting helpers for end-to-end test packages (host, docker, vm).", "type": "module", "private": true, diff --git a/test-packages/e2e-vm-tests/CHANGELOG.md b/test-packages/e2e-vm-tests/CHANGELOG.md new file mode 100644 index 00000000..cb9db960 --- /dev/null +++ b/test-packages/e2e-vm-tests/CHANGELOG.md @@ -0,0 +1,10 @@ +# @podkit/e2e-vm-tests + +## 0.0.1 + +### Patch Changes + +- Updated dependencies [[`0f3e4dd`](https://github.com/jvgomg/podkit/commit/0f3e4ddae134228b5e874b21db33f74547867b6c), [`036b107`](https://github.com/jvgomg/podkit/commit/036b1077748253385b6f4ff873a7cdb52c54b004), [`621b10a`](https://github.com/jvgomg/podkit/commit/621b10abbec3a8e369da9620733210fef4b76f99), [`89ff40c`](https://github.com/jvgomg/podkit/commit/89ff40c2adedd9fec38ae5ad0eb89b75525642f2), [`c5c0236`](https://github.com/jvgomg/podkit/commit/c5c0236c232cc3fa086fd3937b0e2fbe0f326185), [`0d4a4c2`](https://github.com/jvgomg/podkit/commit/0d4a4c2bd98667989b9631d981e609bc72e604af), [`513173d`](https://github.com/jvgomg/podkit/commit/513173d1832bf9ca2894214e97d9d65cf02c52a5), [`679bec8`](https://github.com/jvgomg/podkit/commit/679bec8b0c0e40fc8c6ae253ceaaba87f7ebfd2b), [`0cc39d3`](https://github.com/jvgomg/podkit/commit/0cc39d3c62343591127d5c79deed2478f8dc4f60), [`22dddf4`](https://github.com/jvgomg/podkit/commit/22dddf4803f4cfd7b004d80dffd83878a68b10f2), [`484fb0e`](https://github.com/jvgomg/podkit/commit/484fb0ea63eea297f19217d1acb96163a6754b05), [`348f2c5`](https://github.com/jvgomg/podkit/commit/348f2c53cec06598903b5cf128663d5121c46865), [`7534c2f`](https://github.com/jvgomg/podkit/commit/7534c2f19d81087413af8abbf764fe20cef61384), [`d1147e4`](https://github.com/jvgomg/podkit/commit/d1147e4a65ac103608da3730f530f6deab3cd0b6), [`6747667`](https://github.com/jvgomg/podkit/commit/6747667049cd793fdb13e3d1bc1092651f8e969c), [`8bc3126`](https://github.com/jvgomg/podkit/commit/8bc3126ec415aa836b746ec921b6738abdd9e538), [`87cb87a`](https://github.com/jvgomg/podkit/commit/87cb87aef59ad366b4c6c2b4c22f897f0b84a54a), [`01ecedd`](https://github.com/jvgomg/podkit/commit/01ecedde623ff99e94c5cbda75ff9f9c9ecef632), [`667d66b`](https://github.com/jvgomg/podkit/commit/667d66b90e0979aaff381968358f2cfc78c8e581), [`03f1046`](https://github.com/jvgomg/podkit/commit/03f1046b70898b0282d0c96927bca60ee0d55eeb), [`78b0c71`](https://github.com/jvgomg/podkit/commit/78b0c71b9866306aecbb96f2a0e372a86564f2fc), [`14d83e5`](https://github.com/jvgomg/podkit/commit/14d83e5e59eb0a8a801850de775f9fdb4c0e7aa9), [`a78e5fe`](https://github.com/jvgomg/podkit/commit/a78e5fee4e47293c1935395bb157cb6574782625), [`e0f65f4`](https://github.com/jvgomg/podkit/commit/e0f65f4b0cf4fce28138849b7a85f2c3a7c1a613), [`785ad57`](https://github.com/jvgomg/podkit/commit/785ad57af6627059fe3a6d7e1fef475e82c34764), [`4efa15c`](https://github.com/jvgomg/podkit/commit/4efa15c7e42874e9dd88ef2731230d5314d83f20), [`3db3d88`](https://github.com/jvgomg/podkit/commit/3db3d887ae2cd19d01ba2c1f00b8682e783fac84), [`7ebb7c5`](https://github.com/jvgomg/podkit/commit/7ebb7c5c0e1c7c3d549196347029d9ce660fcb8b), [`34e8bf2`](https://github.com/jvgomg/podkit/commit/34e8bf2341111df1e8f85361b8047eed9f31665a), [`3e95baf`](https://github.com/jvgomg/podkit/commit/3e95baffc65b683b5e3f80906e9a342245a6e4ce), [`bddea04`](https://github.com/jvgomg/podkit/commit/bddea044342ca9027fc95593a35795fd8de1faf4), [`09c4acd`](https://github.com/jvgomg/podkit/commit/09c4acdec349f200a649b2db15fe05345e380a7b), [`151152a`](https://github.com/jvgomg/podkit/commit/151152ae835529730b3235a780550ec35ad685e2), [`94c85d2`](https://github.com/jvgomg/podkit/commit/94c85d2a9d6c85875432a0ebecab540a9ebd67d7), [`efa14c6`](https://github.com/jvgomg/podkit/commit/efa14c623e7bda81066bd77142cddb28e4de615d), [`208e482`](https://github.com/jvgomg/podkit/commit/208e482db9730064a25e53e03121bdcfcbea6341), [`2a644af`](https://github.com/jvgomg/podkit/commit/2a644afa386dd091e8268c8db7dac906c48e44d8), [`f5d0082`](https://github.com/jvgomg/podkit/commit/f5d00829f3b1a80453bdc4f7e6599566f7f02bb3), [`4ee5e2b`](https://github.com/jvgomg/podkit/commit/4ee5e2be470a93a54c2d54bc0aab257d7b92babe), [`303c35a`](https://github.com/jvgomg/podkit/commit/303c35aea57c0f35f64481e12e5cb9298e9a5631), [`bb96778`](https://github.com/jvgomg/podkit/commit/bb96778dde9063267188b2b83535ec279cd5c550), [`7517a24`](https://github.com/jvgomg/podkit/commit/7517a2444abf629f8e032faf29c938eb74b9b51b), [`275c972`](https://github.com/jvgomg/podkit/commit/275c97295462547037e2c911c139654eb50d4af7), [`cac7fc1`](https://github.com/jvgomg/podkit/commit/cac7fc123861e97b10d31c83728a1e3f0431934e), [`f72fa01`](https://github.com/jvgomg/podkit/commit/f72fa0170872fc0a6e5719b4509abae24e6414cd), [`7bf7127`](https://github.com/jvgomg/podkit/commit/7bf7127d3141ce4b91138e3284b18aa5e8ea5984), [`c9c268e`](https://github.com/jvgomg/podkit/commit/c9c268ea4b25b39543e5c53a1928e72b4c31e0c8), [`1ec30ac`](https://github.com/jvgomg/podkit/commit/1ec30acca1109178012db3913a60967a2087fb5b), [`80fe65a`](https://github.com/jvgomg/podkit/commit/80fe65a022c65da512f571a8abf83f9385a649e6), [`63a69d1`](https://github.com/jvgomg/podkit/commit/63a69d11160770bcc5e251c7faf14d5c8887af13), [`52894c1`](https://github.com/jvgomg/podkit/commit/52894c1977bccd51a86929debfbaa7028a19dd61), [`c5cba69`](https://github.com/jvgomg/podkit/commit/c5cba6998283663b42659f02b17b194ab256c137), [`1c3ebc3`](https://github.com/jvgomg/podkit/commit/1c3ebc381276accdb8361f50454b90c75f2391df), [`cdebfb3`](https://github.com/jvgomg/podkit/commit/cdebfb3512f347356bc661722d2236b359776372), [`ec8dc85`](https://github.com/jvgomg/podkit/commit/ec8dc8549447b0178a8746b8cda2b8b7908b9d04), [`f61a83b`](https://github.com/jvgomg/podkit/commit/f61a83b3a2d13612730f174759fd3b86edd42e82), [`6000868`](https://github.com/jvgomg/podkit/commit/6000868830d9437a6fff3c1a77adb254d9579fe7), [`e825ee1`](https://github.com/jvgomg/podkit/commit/e825ee1dd4933ecbfd070dda27f96f43056f0baf)]: + - @podkit/core@0.7.0 + - @podkit/device-types@0.1.0 + - @podkit/device-testing@0.0.2 diff --git a/test-packages/e2e-vm-tests/package.json b/test-packages/e2e-vm-tests/package.json index fef5af16..a00e0441 100644 --- a/test-packages/e2e-vm-tests/package.json +++ b/test-packages/e2e-vm-tests/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/e2e-vm-tests", - "version": "0.0.0", + "version": "0.0.1", "description": "End-to-end VM tests for podkit features running against the @podkit/device-testing harness", "type": "module", "private": true, diff --git a/test-packages/test-fixtures/CHANGELOG.md b/test-packages/test-fixtures/CHANGELOG.md new file mode 100644 index 00000000..3933720e --- /dev/null +++ b/test-packages/test-fixtures/CHANGELOG.md @@ -0,0 +1,15 @@ +# @podkit/test-fixtures + +## 0.1.0 + +### Minor Changes + +- [`52894c1`](https://github.com/jvgomg/podkit/commit/52894c1977bccd51a86929debfbaa7028a19dd61) Thanks [@jvgomg](https://github.com/jvgomg)! - `@podkit/test-fixtures`: expose synthetic-track generators as a library + + Adds a library entry (`src/lib.ts`) exposing `generateMiniFlac`, `generateMiniMp3`, `generateMiniM4a`, `generateMiniOggVorbis`, and `generateMiniOggOpus`. Each helper writes a single short sine-tone file in the requested codec/container with optional metadata. Integration tests that need real audio for tag round-trip coverage now import these from `@podkit/test-fixtures` rather than re-implementing the ffmpeg invocation inline. + + Each generator calls a new `requireEncoder()` guard before invoking ffmpeg. If the host's ffmpeg is missing the codec's encoder, the helper throws a clear error with platform-aware install hints. There is also an explicit `bun run --filter @podkit/test-fixtures check-ffmpeg` script that verifies the full set of required encoders against the host environment in one shot. + + The mass-storage tag writer integration test (`packages/podkit-core/src/device/mass-storage-tag-writer.integration.test.ts`) drops its inline `generateOgg`, `generateOpus`, `generateFlac`, `generateM4a`, `generateMp3` helpers and the `HAS_LIBVORBIS` skip predicate. The OGG Vorbis tests now run unconditionally — they fail loudly with an install hint when libvorbis is absent rather than skipping silently. + + Developer docs (`docs/developers/development.md`) updated to point macOS contributors at the `homebrew-ffmpeg/ffmpeg` tap for full encoder coverage. diff --git a/test-packages/test-fixtures/package.json b/test-packages/test-fixtures/package.json index 056e9c8c..960c9969 100644 --- a/test-packages/test-fixtures/package.json +++ b/test-packages/test-fixtures/package.json @@ -1,6 +1,6 @@ { "name": "@podkit/test-fixtures", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "main": "./dist/lib.js",