Files
-rpi-sendspin/third_party/sendspin-go/docs/superpowers/plans/2026-04-14-rip-legacy-cli-stack.md

751 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Rip Legacy CLI Stack Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Delete `cmd/ma-player`, `cmd/test-sync`, and the entire `internal/` legacy player pipeline they depend on. Unify on the `pkg/sendspin.Player` + `sendspin-player` code path as the only player in the tree.
**Architecture:** `cmd/ma-player` and `cmd/test-sync` both call `internal/app.Player`, which wires together `internal/client`, `internal/audio`, `internal/player` (scheduler), `internal/artwork`, and `internal/sync` into a parallel player pipeline that predates `pkg/sendspin`. Since PR #26 migrated `internal/app.Player`'s output layer to `pkg/audio/output.Malgo`, `ma-player` is functionally equivalent to `sendspin-player` — both negotiate the same supported-formats list, both route 24-bit hi-res through malgo, both connect to Music Assistant cleanly. Empirically verified: `sendspin-player.exe` connects to Music Assistant and plays 192kHz/24-bit successfully (log evidence in `sendspin-player.log` from v1.2.0 verification). This plan removes the duplicate pipeline and its supporting packages, then deletes the `pkg/sync.SetGlobalClockSync`/`ServerMicrosNow` deprecation shims that only existed to keep the legacy readers working.
**Tech Stack:** Go 1.24+, existing `pkg/sendspin`, `pkg/sync`, `pkg/audio`, `pkg/protocol`, `pkg/discovery`.
**Context:**
- Resolves issue #28 (sync deprecation migration).
- Partially addresses issue #35 (release workflow only builds `ma-player` + `sendspin-player` Linux binaries — after this, only `sendspin-player` remains to ship).
- Does NOT fix issue #27 (unknown binary type 8), #29 (memory stats), #30 (clock offset display), #31 (duplicate Resampler), #32 (hello log spam), #33 (resonate-artwork cache dir), #34 (FLAC stub) — those are separate and unrelated to this cleanup.
- The `docs/superpowers/plans/2026-04-12-layered-architecture.md` plan is scoped to `pkg/sendspin` and does not overlap with this work.
**Out of scope:**
- Any refactor of `pkg/sendspin.Player` itself.
- Deletion of `internal/server/` — that package is still live and imported by `cmd/sendspin-server` and `pkg/sendspin/server.go`.
- Deletion of `internal/discovery/` — still imported by `internal/server`, `main.go`, and `pkg/sendspin`.
- Deletion of `internal/version/` — still imported by `main.go`.
- Deletion of `internal/ui/` — still imported by `main.go` for the TUI.
---
## Dependency Audit Summary
Reverse-import scan before writing this plan confirmed the following (all grep paths relative to repo root):
| Package | Non-legacy importers (survive) | Legacy importers (die with `internal/app`) |
|---|---|---|
| `internal/app` | (none) | `cmd/ma-player/main.go`, `cmd/test-sync/main.go` |
| `internal/audio` | (none) | `internal/app/player.go`, `internal/player/scheduler.go` |
| `internal/client` | (none) | `internal/app/player.go` |
| `internal/player` | (none) | `internal/app/player.go` |
| `internal/artwork` | (none) | `internal/app/player.go` |
| `internal/protocol` | (none — already orphaned) | (none) |
| `internal/sync` | `main.go` (Quality enum only), `internal/ui/model.go`, `internal/ui/model_test.go` | `internal/app/player.go`, `internal/player/scheduler.go` |
| `internal/ui` | `main.go` | `internal/app/player.go` |
| `internal/version` | `main.go` | `internal/app/player.go` |
| `internal/discovery` | `internal/server/server.go`, `pkg/sendspin/{client_dialer,client_dialer_test,client_discovery_integration_test,server}.go`, `main.go` | `internal/app/player.go` |
| `internal/server` | `cmd/sendspin-server/main.go`, `pkg/sendspin/server.go` | (none) |
**Key observation:** `pkg/sync.Quality` is byte-for-byte equivalent to `internal/sync.Quality` (same type, same three constants in the same iota order). `main.go`'s current `pkg/sync.Quality``internal/sync.Quality` conversion block (lines 207216) is pure ceremony. Migrating the three callers of `internal/sync.Quality` (`internal/ui/model.go`, `internal/ui/model_test.go`, `main.go`) to `pkg/sync.Quality` allows `internal/sync/` to be deleted after `internal/app` and `internal/player` go.
---
## File Structure
**Delete entirely:**
| Path | Lines | Notes |
|---|---|---|
| `cmd/ma-player/main.go` | ~90 | Duplicate of root `main.go` via legacy stack |
| `cmd/test-sync/main.go` | ~50 | Clock-sync debug harness, unused |
| `internal/app/player.go` | 567 | Legacy orchestrator |
| `internal/app/player_test.go` | 229 | |
| `internal/audio/decoder.go` | 125 | Duplicate of `pkg/audio/decode` |
| `internal/audio/decoder_test.go` | 64 | |
| `internal/audio/types.go` | 59 | |
| `internal/client/websocket.go` | 378 | Duplicate of `pkg/protocol.Client` |
| `internal/client/websocket_test.go` | 24 | |
| `internal/player/scheduler.go` | 238 | Duplicate of `pkg/sendspin.Scheduler` |
| `internal/player/scheduler_test.go` | 44 | |
| `internal/artwork/downloader.go` | 108 | Not used by `pkg/sendspin.Player` today |
| `internal/artwork/downloader_test.go` | 278 | |
| `internal/sync/clock.go` | 139 | Duplicate of `pkg/sync/clock.go` |
| `internal/sync/clock_test.go` | (varies) | |
| `internal/protocol/messages.go` | 130 | Already orphaned (no non-internal importers) |
| `internal/protocol/messages_test.go` | 79 | |
**Modify:**
| Path | Change |
|---|---|
| `internal/ui/model.go` | Replace `internal/sync` import with `pkg/sync`; type of `syncQuality` and `StatusMsg.SyncQuality` unchanged in name, changes in import source |
| `internal/ui/model_test.go` | Same replacement |
| `main.go` | Drop `internalsync "github.com/Sendspin/sendspin-go/internal/sync"` import; delete the Quality conversion block at ~lines 207216; pass `stats.SyncQuality` directly to `ui.StatusMsg` |
| `pkg/sync/clock.go` | Delete package-level deprecated `SetGlobalClockSync`, `ServerMicrosNow`, and the `globalClockSync` / `globalDeprecationWarned` package variables |
| `pkg/sendspin/player.go` | Delete the `sync.SetGlobalClockSync(recv.ClockSync())` compatibility-shim call (currently at line 157 per PR #26 final state) |
| `.github/workflows/release.yml` | Remove `ma-player` from build matrix if present |
| `Makefile` | Remove any `ma-player` or `test-sync` targets if present |
Total deletion: ~25002600 lines across ~17 files, plus small modifications to 5 surviving files.
---
## Pre-flight: Branch setup
- [ ] **Step 0a: Start from clean main**
```bash
git checkout main
git pull --ff-only
git worktree add -b feat/rip-legacy-cli ../sendspin-go-worktrees/rip-legacy-cli origin/main
cd ../sendspin-go-worktrees/rip-legacy-cli
```
- [ ] **Step 0b: Verify baseline green before any changes**
```bash
export PATH="/c/msys64/mingw64/bin:$PATH"
go build ./...
go test -count=1 ./... 2>&1 | tail -25
```
Expected: all packages pass. If anything is red before you start, stop and investigate.
- [ ] **Step 0c: Commit the plan doc on the feature branch**
```bash
# If the plan file is on main, pull it in. If the plan lives elsewhere, cp or cherry-pick.
# Assuming the plan file is already at docs/superpowers/plans/2026-04-14-rip-legacy-cli-stack.md on main:
git log --oneline main -- docs/superpowers/plans/2026-04-14-rip-legacy-cli-stack.md
# If the file exists, it's already on the branch via the fresh origin/main base.
# If not, commit it explicitly:
# git add docs/superpowers/plans/2026-04-14-rip-legacy-cli-stack.md
# git commit -m "docs: add rip-legacy-cli implementation plan"
```
---
### Task 1: Migrate `internal/ui` to `pkg/sync.Quality`
`internal/ui/model.go` and its test file both import `internal/sync` for the `Quality` enum. Switch them to `pkg/sync`, which has the same type and the same constants (`QualityGood`, `QualityDegraded`, `QualityLost`). After this task, `internal/ui` no longer imports `internal/sync`.
**Files:**
- Modify: `internal/ui/model.go`
- Modify: `internal/ui/model_test.go`
- [ ] **Step 1: Update the import in `internal/ui/model.go`**
Find the line:
```go
"github.com/Sendspin/sendspin-go/internal/sync"
```
Replace with:
```go
"github.com/Sendspin/sendspin-go/pkg/sync"
```
The package name used in the file is `sync` in both cases (no alias needed). All existing references like `sync.Quality`, `sync.QualityGood`, `sync.QualityDegraded`, `sync.QualityLost` continue to resolve correctly because `pkg/sync` has the same identifiers.
- [ ] **Step 2: Update the import in `internal/ui/model_test.go`**
Same replacement. If the test file uses `sync.Quality*` constants they all resolve against the new package identically.
- [ ] **Step 3: Verify `internal/ui` builds and tests pass**
```bash
go build ./internal/ui/
go test ./internal/ui/
```
Expected: clean build, all tests pass.
- [ ] **Step 4: Verify `main.go` still builds**
```bash
go build .
```
Expected: clean. `main.go` still converts `pkg/sync.Quality``internal/sync.Quality` in its own code at this point; that conversion keeps working because `internal/sync` is still alive.
- [ ] **Step 5: Commit**
```bash
git add internal/ui/model.go internal/ui/model_test.go
git commit -m "refactor(internal/ui): use pkg/sync.Quality instead of internal/sync.Quality
pkg/sync.Quality is byte-for-byte equivalent to internal/sync.Quality
(same type, same three constants in the same iota order). Switching
the TUI's only internal/sync consumer is step 1 of deleting the
legacy internal/ stack."
```
---
### Task 2: Drop the `internal/sync` conversion layer in `main.go`
`main.go` currently imports both `pkg/sync` (as `sync`) and `internal/sync` (as `internalsync`), and explicitly converts `pkg/sync.Quality` to `internal/sync.Quality` before constructing a `ui.StatusMsg`. After Task 1, `ui.StatusMsg.SyncQuality` is typed as `pkg/sync.Quality`, so the conversion is dead ceremony. Remove it and drop the `internal/sync` import.
**Files:**
- Modify: `main.go`
- [ ] **Step 1: Remove the `internalsync` import**
Find in the import block:
```go
internalsync "github.com/Sendspin/sendspin-go/internal/sync"
```
Delete that line.
- [ ] **Step 2: Remove the Quality conversion block**
Find in `statsUpdateLoop` (currently around lines 207216):
```go
// Convert pkg/sync.Quality to internal/sync.Quality
var syncQuality internalsync.Quality
switch stats.SyncQuality {
case 0: // QualityGood
syncQuality = internalsync.QualityGood
case 1: // QualityDegraded
syncQuality = internalsync.QualityDegraded
case 2: // QualityLost
syncQuality = internalsync.QualityLost
}
```
Delete the entire block.
Then find the `ui.StatusMsg` construction that uses the `syncQuality` local variable (a few lines below the deleted block):
```go
updateTUI(ui.StatusMsg{
Received: stats.Received,
Played: stats.Played,
Dropped: stats.Dropped,
BufferDepth: stats.BufferDepth,
SyncRTT: stats.SyncRTT,
SyncQuality: syncQuality,
Goroutines: runtime.NumGoroutine(),
MemAlloc: 0,
MemSys: 0,
})
```
Change the `SyncQuality: syncQuality,` line to:
```go
SyncQuality: stats.SyncQuality,
```
(Passing `pkg/sync.Quality` directly, now that `ui.StatusMsg.SyncQuality` is typed as `pkg/sync.Quality` after Task 1.)
- [ ] **Step 3: Verify the root binary builds**
```bash
go build .
```
Expected: clean build, produces `sendspin-player` (or `sendspin-player.exe` on Windows). If you see `undefined: internalsync`, a reference was missed — grep `internalsync` in `main.go` and remove any remaining hits.
- [ ] **Step 4: Verify full build and tests**
```bash
go build ./...
go test ./... 2>&1 | tail -25
```
Expected: all packages still pass. `internal/sync` is still present and still imported by `internal/app/player.go` and `internal/player/scheduler.go` — those die in a later task.
- [ ] **Step 5: Commit**
```bash
git add main.go
git commit -m "refactor(main): drop internal/sync conversion layer
ui.StatusMsg.SyncQuality is now pkg/sync.Quality (see prior commit),
so the pkg/sync -> internal/sync conversion block is pure ceremony.
Pass stats.SyncQuality through directly."
```
---
### Task 3: Delete `cmd/ma-player` and `cmd/test-sync`
Both CLIs import `internal/app`, which is the only non-legacy consumer of that package. Deleting them orphans `internal/app` for deletion in the next task.
**Files:**
- Delete: `cmd/ma-player/main.go`
- Delete: `cmd/test-sync/main.go`
- [ ] **Step 1: Confirm these are the only importers of `internal/app`**
```bash
grep -rln "\"github.com/Sendspin/sendspin-go/internal/app\"" --include='*.go' .
```
Expected output (exactly):
```
./cmd/ma-player/main.go
./cmd/test-sync/main.go
```
If there are other hits, STOP and report as BLOCKED — the scope was assumed wrong.
- [ ] **Step 2: Delete the CLI directories**
```bash
git rm -r cmd/ma-player cmd/test-sync
```
- [ ] **Step 3: Check the Makefile for references**
```bash
grep -n "ma-player\|test-sync" Makefile
```
If any targets or recipes name `ma-player` or `test-sync`, delete those lines. The existing `build`, `server`, `player`, `test`, `lint`, `clean`, `install` targets should be unaffected. Example: if you see
```makefile
ma-player:
go build -o ma-player ./cmd/ma-player
```
delete the target and any references to it in the phony list or the top-level `all:`/`build:` dependency lists.
- [ ] **Step 4: Check the release workflow for references**
```bash
grep -n "ma-player\|test-sync" .github/workflows/release.yml
```
If any build steps name these binaries, delete them. This partially addresses issue #35 (release workflow gap) by removing the entries that will no longer exist.
- [ ] **Step 5: Verify the full module still builds**
```bash
go build ./...
```
Expected: clean. Nothing compiles against `cmd/ma-player` or `cmd/test-sync` (they were standalone `package main` binaries), so the rest of the tree builds fine.
- [ ] **Step 6: Verify `internal/app` now has no importers**
```bash
grep -rln "\"github.com/Sendspin/sendspin-go/internal/app\"" --include='*.go' .
```
Expected output: empty (no hits).
- [ ] **Step 7: Commit**
```bash
git add -A cmd/ Makefile .github/workflows/release.yml
git commit -m "refactor: delete cmd/ma-player and cmd/test-sync
ma-player duplicated sendspin-player's functionality via the legacy
internal/app stack; verified empirically that sendspin-player.exe
handles the Music Assistant case cleanly, including 192kHz/24-bit
PCM hi-res streaming (v1.2.0 verification logs). test-sync was a
one-off clock-sync debug harness with no remaining users. Removing
both is step 1 of deleting the duplicate internal/ pipeline."
```
---
### Task 4: Delete `internal/app`
After Task 3, no caller remains. `internal/app/player.go` and its test file both die. This orphans `internal/audio`, `internal/client`, `internal/player`, `internal/artwork`, `internal/version`, and `internal/discovery` of their legacy consumer — though some of those packages (version, discovery) still have non-legacy consumers and survive.
**Files:**
- Delete: `internal/app/player.go`
- Delete: `internal/app/player_test.go`
- [ ] **Step 1: Confirm `internal/app` still has zero importers**
```bash
grep -rln "\"github.com/Sendspin/sendspin-go/internal/app\"" --include='*.go' .
```
Expected output: empty.
- [ ] **Step 2: Delete the package**
```bash
git rm -r internal/app
```
- [ ] **Step 3: Verify build and test**
```bash
go build ./...
go test -count=1 ./... 2>&1 | tail -25
```
Expected: all surviving packages build and pass. `internal/audio`, `internal/client`, `internal/player`, `internal/artwork` will still build independently — their tests continue to pass within their own packages because they don't depend on `internal/app`. The one surprise that could happen is if any package depends on `internal/app` in a way the grep missed (e.g., via test helpers or a build-tagged file). If the build fails, STOP and report.
- [ ] **Step 4: Commit**
```bash
git add -A internal/app/
git commit -m "refactor(internal/app): delete legacy player orchestrator
Only callers were cmd/ma-player and cmd/test-sync, both deleted in
the previous commit. This removes ~800 lines of duplicate pipeline
wiring that predated pkg/sendspin."
```
---
### Task 5: Delete `internal/player`
`internal/player/scheduler.go` was only used by `internal/app`. After Task 4, it has no importers. This is the last non-test consumer of `internal/audio` and (along with the earlier `internal/ui`/`main.go` migration) of `internal/sync`.
**Files:**
- Delete: `internal/player/scheduler.go`
- Delete: `internal/player/scheduler_test.go`
- [ ] **Step 1: Confirm `internal/player` has zero importers**
```bash
grep -rln "\"github.com/Sendspin/sendspin-go/internal/player\"" --include='*.go' .
```
Expected output: empty.
- [ ] **Step 2: Delete the package**
```bash
git rm -r internal/player
```
- [ ] **Step 3: Verify build and test**
```bash
go build ./...
go test -count=1 ./... 2>&1 | tail -25
```
Expected: all packages pass.
- [ ] **Step 4: Commit**
```bash
git add -A internal/player/
git commit -m "refactor(internal/player): delete legacy scheduler
The only caller, internal/app, was deleted in the previous commit.
pkg/sendspin has its own Scheduler that serves the modern player
pipeline."
```
---
### Task 6: Delete the remaining orphaned `internal/` packages
After Tasks 4 and 5, the following packages have no importers anywhere in the tree:
- `internal/audio/` (decoder.go, decoder_test.go, types.go)
- `internal/client/` (websocket.go, websocket_test.go)
- `internal/artwork/` (downloader.go, downloader_test.go)
- `internal/sync/` (clock.go, clock_test.go)
- `internal/protocol/` (messages.go, messages_test.go) — already orphaned before this PR; deleting it now finishes the sweep
**Files:**
- Delete: `internal/audio/` (directory)
- Delete: `internal/client/` (directory)
- Delete: `internal/artwork/` (directory)
- Delete: `internal/sync/` (directory)
- Delete: `internal/protocol/` (directory)
- [ ] **Step 1: Confirm every target package has zero importers**
```bash
for pkg in audio client artwork sync protocol; do
echo "=== internal/$pkg ==="
grep -rln "\"github.com/Sendspin/sendspin-go/internal/$pkg\"" --include='*.go' . || echo "(clean)"
done
```
Expected: all five should print `(clean)`. If any one still has importers, STOP and report — earlier tasks missed something.
- [ ] **Step 2: Delete the five packages**
```bash
git rm -r internal/audio internal/client internal/artwork internal/sync internal/protocol
```
- [ ] **Step 3: Verify build and test**
```bash
go build ./...
go test -count=1 ./... 2>&1 | tail -25
```
Expected: all surviving packages pass. The only remaining `internal/` packages should now be:
```bash
ls internal/
# expected: discovery server ui version
```
- [ ] **Step 4: Commit**
```bash
git add -A internal/
git commit -m "refactor: delete orphaned internal/ packages
After removing internal/app and internal/player, the following
packages have no remaining importers in the tree:
- internal/audio (duplicate of pkg/audio/decode)
- internal/client (duplicate of pkg/protocol.Client)
- internal/artwork (unused by pkg/sendspin.Player)
- internal/sync (duplicate of pkg/sync)
- internal/protocol (already orphaned pre-PR)
Surviving internal/ packages: discovery, server, ui, version."
```
---
### Task 7: Delete the `pkg/sync` deprecation shims
With `internal/player/scheduler.go` and `internal/app/player.go` deleted, nothing in the tree reads from the package-level `sync.ServerMicrosNow()` or writes to the global via `sync.SetGlobalClockSync()`. The compatibility shim at `pkg/sendspin/player.go` (line ~157 per PR #26 final state) also becomes dead — it was only there so the `internal/` readers could see the `ClockSync` via the global.
This resolves issue #28 (sync deprecation migration) in full.
**Files:**
- Modify: `pkg/sync/clock.go`
- Modify: `pkg/sendspin/player.go`
- [ ] **Step 1: Confirm no caller of `sync.ServerMicrosNow()` or `sync.SetGlobalClockSync`**
```bash
grep -rn "sync\.ServerMicrosNow\|sync\.SetGlobalClockSync" --include='*.go' . | grep -v "_test.go" | grep -v "pkg/sync/clock.go"
```
Expected: no hits in production code. Test files may still reference the deprecated symbols if the `pkg/sync` package's own tests exercise them — those will need updating in Step 3 if so.
- [ ] **Step 2: Delete the deprecated package-level functions from `pkg/sync/clock.go`**
In `pkg/sync/clock.go`, find and delete these three sections:
```go
// Deprecated: ServerMicrosNow returns current time in server's reference frame (us).
// Use ClockSync.ServerMicrosNow() on the instance from Receiver.ClockSync() instead.
func ServerMicrosNow() int64 {
cs := globalClockSync
// ... (entire function body)
}
```
```go
var (
globalClockSync *ClockSync
globalDeprecationWarned bool
)
// Deprecated: SetGlobalClockSync sets the global clock sync instance.
// Use Receiver.ClockSync() instead for new code.
func SetGlobalClockSync(cs *ClockSync) {
if !globalDeprecationWarned {
log.Printf("Warning: SetGlobalClockSync is deprecated, use Receiver.ClockSync() instead")
globalDeprecationWarned = true
}
globalClockSync = cs
}
```
Delete both declarations (`var (...)` block and both functions) entirely. Do NOT touch the instance method `(cs *ClockSync) ServerMicrosNow()` — that one stays; it's the replacement API.
After the delete, check whether `pkg/sync/clock.go` still imports `"log"`. If `log.Printf` is no longer called anywhere in the file, `go build` will flag the unused import — remove it.
- [ ] **Step 3: Update `pkg/sync/clock_test.go` if it references the deleted symbols**
```bash
grep -n "ServerMicrosNow\b\|SetGlobalClockSync\|globalClockSync" pkg/sync/clock_test.go
```
If the test file references the package-level `ServerMicrosNow()` (no receiver) or `SetGlobalClockSync`, those tests need to migrate to the instance method `cs.ServerMicrosNow()` or be deleted. In particular, if there's a `TestServerMicrosNow` (package-level) AND a `TestClockSync_ServerMicrosNow` (instance method), keep the latter and delete the former.
If `pkg/sync/clock_test.go` has no references to the deleted symbols, skip this step.
- [ ] **Step 4: Delete the compat shim from `pkg/sendspin/player.go`**
In `pkg/sendspin/player.go`, find the line (currently around line 157, inside `Connect()`):
```go
// Backward compat: set global clock sync
sync.SetGlobalClockSync(recv.ClockSync())
```
Delete both lines (the comment and the call).
After the delete, check whether `pkg/sendspin/player.go` still uses the `"github.com/Sendspin/sendspin-go/pkg/sync"` import for anything. If `sync.` no longer appears in the file, remove the import. If it appears for other types (e.g., `sync.Quality`), leave the import alone.
- [ ] **Step 5: Verify build and test**
```bash
go build ./...
go test -count=1 ./... 2>&1 | tail -25
```
Expected: all packages pass. Particularly `pkg/sync`, `pkg/sendspin`, and `main.go` all still build cleanly.
- [ ] **Step 6: Grep to confirm the deprecation warning string is gone**
```bash
grep -rn "SetGlobalClockSync is deprecated" --include='*.go' .
```
Expected: no hits. The deprecation warning no longer exists because the function that emits it no longer exists.
- [ ] **Step 7: Commit**
```bash
git add pkg/sync/clock.go pkg/sync/clock_test.go pkg/sendspin/player.go
git commit -m "refactor(pkg/sync): delete deprecated global clock-sync shims
The only callers of sync.ServerMicrosNow() (package-level) and
sync.SetGlobalClockSync() lived in the internal/ legacy stack,
which was deleted in earlier commits. The instance method
(*ClockSync).ServerMicrosNow() is now the only way to read
server-frame time.
Resolves #28."
```
---
### Task 8: Final sweep and `go mod tidy`
Dropping the `internal/` packages may have orphaned module dependencies that were only pulled in by the legacy stack. Run `go mod tidy` to clean `go.mod` and `go.sum`.
**Files:**
- Modify: `go.mod`, `go.sum` (if anything changed)
- [ ] **Step 1: Run `go mod tidy`**
```bash
go mod tidy
```
- [ ] **Step 2: Review the diff**
```bash
git diff go.mod go.sum
```
If `go.mod` / `go.sum` are unchanged, skip steps 3 and 4. If they are changed, read the diff — you should only see REMOVALS (a dependency that `internal/client` or `internal/audio` was the only user of). Any ADDITION is suspicious; investigate before committing.
Candidate dependencies that may disappear:
- Anything the legacy `internal/client/websocket.go` used that `pkg/protocol/client.go` doesn't
- Anything `internal/audio/decoder.go` used that `pkg/audio/decode/` doesn't
- [ ] **Step 3: Verify final build and full test suite**
```bash
go build ./...
go test -count=1 ./... 2>&1 | tail -25
```
Expected: clean + all packages pass.
- [ ] **Step 4: Commit (skip if `go mod tidy` was a no-op)**
```bash
git add go.mod go.sum
git commit -m "chore(deps): go mod tidy after legacy stack removal"
```
- [ ] **Step 5: Final structural check**
```bash
ls internal/
# expected: discovery server ui version
find cmd -type d
# expected: cmd cmd/sendspin-server
```
- [ ] **Step 6: Sanity: grep for any string references to deleted binaries**
```bash
grep -rn "ma-player\|test-sync" --include='*.md' --include='*.yml' --include='*.sh' --include='Makefile' .
```
Review every hit. Historical notes in `CHANGELOG.md`, `docs/PHASE1_IMPLEMENTATION.md`, and frozen plan docs under `docs/superpowers/plans/` may legitimately mention them — leave those alone. Current-state references (instructions, scripts, CI configs) need cleanup. Commit any doc updates as a separate trailing commit if needed:
```bash
git add <any updated docs>
git commit -m "docs: remove stale references to deleted ma-player/test-sync"
```
---
### Task 9: Open the PR
- [ ] **Step 1: Push the feature branch**
```bash
git push -u origin feat/rip-legacy-cli
```
- [ ] **Step 2: Open the PR**
```bash
gh pr create --title "refactor: delete legacy internal/ CLI stack (ma-player, test-sync, internal/app and deps)" --body "$(cat <<'EOF'
## Summary
Delete \`cmd/ma-player\`, \`cmd/test-sync\`, and the entire \`internal/\` legacy player pipeline they depended on:
- \`cmd/ma-player/\`, \`cmd/test-sync/\`
- \`internal/app/\`, \`internal/audio/\`, \`internal/client/\`, \`internal/player/\`, \`internal/artwork/\`, \`internal/sync/\`, \`internal/protocol/\`
Surviving \`internal/\` packages: \`discovery\`, \`server\`, \`ui\`, \`version\`.
Also removes the \`pkg/sync.SetGlobalClockSync\` / \`ServerMicrosNow\` package-level deprecation shims (and the compat-shim call in \`pkg/sendspin.Player.Connect\`), now that no readers remain. Resolves #28.
## Why
\`ma-player\` duplicated \`sendspin-player\`'s functionality via the legacy \`internal/\` stack. After PR #26 migrated \`internal/app.Player\`'s output layer to \`pkg/audio/output.Malgo\`, the only difference between the two binaries was that one went through \`pkg/sendspin.Receiver\` and the other went through \`internal/app.Player\` → \`internal/client\` → \`internal/player.Scheduler\`. Both negotiate the same hi-res-first format list, both route 24-bit PCM through malgo, both connect to Music Assistant. Empirically verified: \`sendspin-player.exe\` was tested end-to-end against a real Music Assistant server at 192kHz/24-bit during the v1.2.0 verification pass — log evidence in \`sendspin-player.log\`.
\`test-sync\` was a one-off clock-sync debugging CLI from the earlier "make server sync work" era; nobody runs it today. It's in git history if anyone needs it back.
## Scope
- **In:** delete the two CLIs and the seven orphaned \`internal/\` packages; migrate \`internal/ui\` and \`main.go\` off \`internal/sync.Quality\` onto \`pkg/sync.Quality\`; delete the \`pkg/sync\` deprecation shims.
- **Out:** \`internal/server\` (still used by \`cmd/sendspin-server\` and \`pkg/sendspin/server.go\`), \`internal/discovery\`, \`internal/ui\`, \`internal/version\` — all still have non-legacy consumers.
## Stats
~25002600 lines deleted across ~17 files. No new functionality, no behavioural change to \`sendspin-player\` or \`sendspin-server\`.
## Resolves
- #28 — deprecated \`sync.SetGlobalClockSync\` / \`sync.ServerMicrosNow\` shims removed along with their last readers.
- Partially addresses #35 — release workflow now only needs to ship \`sendspin-player\` and \`sendspin-server\` (ma-player gone).
## Test plan
- [x] \`go build ./...\` clean
- [x] \`go test -count=1 ./...\` green on every surviving package
- [ ] **Manual playback verification** — run \`./sendspin-player.exe --server <addr>\` against a real server and confirm audio plays, volume/mute work, TUI shows expected state. The expected behavior is identical to v1.2.0 since \`sendspin-player\` was untouched by this PR.
## Rollback
\`git revert\` the PR merge commit. All deletions are in one linear chain; a revert restores the full legacy stack with no partial state.
EOF
)"
```
---
## Verification Checklist (before merging the PR)
- [ ] `go build ./...` clean on a fresh checkout
- [ ] `go test -count=1 ./...` green
- [ ] `ls internal/` shows exactly `discovery server ui version`
- [ ] `ls cmd/` shows exactly `sendspin-server`
- [ ] `grep -rn "internal/app\|internal/client\|internal/player\|internal/audio\|internal/artwork\|internal/sync\|internal/protocol" --include='*.go' .` returns no hits
- [ ] `grep -rn "SetGlobalClockSync\|package-level ServerMicrosNow" --include='*.go' .` returns no hits
- [ ] `./sendspin-player` plays audio end-to-end against a real server (manual)
- [ ] `./sendspin-server` accepts a client and streams audio (manual)
## Rollback Plan
If anything turns out to depend on a deleted symbol and can't be trivially replaced:
1. `git revert` the PR merge commit. The full chain was designed as one revertable unit.
2. `go mod tidy` restores any dependencies that got pruned.
3. The legacy stack returns to life with no partial state.
The most likely sources of trouble, in order of probability:
1. An external consumer of the library that imports `internal/` (shouldn't happen — `internal/` packages are not importable from outside the module, by Go spec).
2. A build-tagged or generated file that references one of the deleted packages (extremely unlikely in this repo; grep confirmed no such files).
3. A script in `scripts/` or a CI workflow that invokes `ma-player` or `test-sync` binaries directly (Task 3 Step 4 sweeps for these).
None of these are expected to actually bite.