🎉 live server seems to be working now

This commit is contained in:
2026-05-14 14:29:57 +02:00
commit d72e439fd9
181 changed files with 47406 additions and 0 deletions

View File

@@ -0,0 +1,750 @@
# 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.