chore: scaffold tvctl foundation
Set up the Rust crate, baseline module layout, and project docs so the repository matches the design bundle and builds cleanly as a starting point.
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
# AGENT.md
|
||||
# Instructions for AI Agents Working on tvctl
|
||||
# Read this file completely before writing any code.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Orient Yourself
|
||||
|
||||
Before doing anything else, read these files in this order:
|
||||
|
||||
1. `PROJECT_MAP.md` — architecture, decisions, data shapes, command tree, API surface
|
||||
2. `ROADMAP.md` — current milestone, what's done, what's next
|
||||
3. `README.md` — user-facing documentation (understand what you're building toward)
|
||||
|
||||
Do not skip any of these. Do not begin coding until you have read all three.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Understand What You Must Not Change
|
||||
|
||||
The following decisions are final and were made intentionally during design.
|
||||
Do not re-open, re-litigate, or silently deviate from them:
|
||||
|
||||
- **Resource-verb CLI pattern** — `tvctl device list`, not `tvctl list-devices`
|
||||
- **Unix socket for CLI↔daemon** — not TCP, not pipes
|
||||
- **HTTP API for tool builders** — versioned at `/v1/`, loopback-only default
|
||||
- **Adapter trait** — the exact interface defined in PROJECT_MAP.md
|
||||
- **TOML config** — not YAML, not JSON
|
||||
- **kebab-case key names** — `volume-up`, not `VolumeUp`, not `volume_up`
|
||||
- **JSON response envelope** — `{ "ok": true, "data": {...} }` always
|
||||
- **Organic app cache** — no pre-populated database, grows from live TV data
|
||||
- **User-level systemd service** — not system-level, not root
|
||||
|
||||
If you believe one of these decisions is wrong, document your concern in a
|
||||
comment or note and ask for clarification. Do not silently work around them.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Understand the Codebase Before Touching It
|
||||
|
||||
Before modifying any file:
|
||||
|
||||
1. Read the file you are about to change completely
|
||||
2. Read any files it imports or depends on
|
||||
3. Understand the data flow through the component you are changing
|
||||
|
||||
Do not make changes based on filenames or directory structure alone.
|
||||
|
||||
---
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Rust conventions
|
||||
- Use `thiserror` for error types
|
||||
- Use `tokio` for all async runtime
|
||||
- Use `axum` for HTTP server
|
||||
- Use `clap` (derive API) for CLI
|
||||
- Use `serde` + `serde_json` for all serialization
|
||||
- Use `uuid` crate for UUIDs
|
||||
- Use `chrono` for timestamps
|
||||
- Prefer `anyhow` for application-level error propagation
|
||||
- All public types must have doc comments
|
||||
|
||||
### Error handling
|
||||
- Never use `.unwrap()` in non-test code unless you can prove it cannot fail
|
||||
- Every error returned to the user (CLI or API) must include a `hint` field
|
||||
- CLI errors must suggest the next action — not just report what went wrong
|
||||
- API error `code` values are stable contracts — do not change existing codes
|
||||
|
||||
### CLI output
|
||||
- Human-readable output by default
|
||||
- `--json` flag must work on every command
|
||||
- Errors go to stderr, data goes to stdout
|
||||
- Do not mix human text and JSON in the same output stream
|
||||
|
||||
### API
|
||||
- All endpoints return the standard envelope — no exceptions
|
||||
- `error.code` values are snake_case strings
|
||||
- `error.hint` is optional but strongly encouraged
|
||||
- Never return platform-specific field names at the API surface level
|
||||
|
||||
### Help text
|
||||
Every command must have:
|
||||
1. A one-line description
|
||||
2. A short paragraph of context
|
||||
3. A usage line
|
||||
4. All subcommands/args listed with descriptions
|
||||
5. At least two concrete examples
|
||||
6. A notes section for technical details (if applicable)
|
||||
|
||||
---
|
||||
|
||||
## File Responsibilities
|
||||
|
||||
| File/Directory | Responsibility | Notes |
|
||||
|----------------|----------------|-------|
|
||||
| `src/main.rs` | Binary entry point, daemon vs CLI dispatch | Keep thin |
|
||||
| `src/cli/` | All clap definitions and CLI handlers | No business logic here |
|
||||
| `src/daemon/` | Daemon lifecycle, routing, services | Core of the application |
|
||||
| `src/daemon/registry.rs` | Device registry, persistence | Owns devices.json |
|
||||
| `src/daemon/discovery.rs` | SSDP discovery, polling | Platform-agnostic |
|
||||
| `src/daemon/cache.rs` | App cache, persistence | Per-platform json files |
|
||||
| `src/daemon/state.rs` | In-memory state cache | Never persisted |
|
||||
| `src/api/` | axum HTTP server, route definitions | Thin layer over core |
|
||||
| `src/adapters/mod.rs` | TvAdapter trait, TvKey, shared types | The contract |
|
||||
| `src/adapters/roku/` | Roku ECP implementation | Only place Roku logic lives |
|
||||
|
||||
The CLI and API layers must contain no business logic. They translate
|
||||
user input into core calls and translate core results into output.
|
||||
All logic lives in the daemon and adapter layers.
|
||||
|
||||
---
|
||||
|
||||
## App Resolution Flow
|
||||
|
||||
When a user runs `tvctl app launch netflix` or `POST /v1/devices/{id}/apps/launch`:
|
||||
|
||||
```
|
||||
1. Check per-device installed app list in memory
|
||||
2. If found → launch directly
|
||||
3. If not found → check platform cache (roku.apps.json)
|
||||
4. If found in cache → attempt launch (app might be installed)
|
||||
5. If launch fails → report "not installed", suggest `tvctl app list`
|
||||
6. If not in cache at all → fetch live app list from TV
|
||||
7. Populate platform cache with all returned apps
|
||||
8. Persist to cache file
|
||||
9. Retry launch
|
||||
```
|
||||
|
||||
Name matching is case-insensitive. Users can also pass raw platform IDs with `--id`.
|
||||
|
||||
---
|
||||
|
||||
## Discovery Flow
|
||||
|
||||
```
|
||||
1. tvctld starts
|
||||
2. If discovery.auto_discover = true → run SSDP scan
|
||||
3. SSDP returns device IP addresses
|
||||
4. For each address → instantiate appropriate adapter → call adapter.discover()
|
||||
5. Adapter returns DeviceInfo
|
||||
6. Check if device UUID already exists in registry
|
||||
7. If new → add to registry, use device-reported name as default friendly name
|
||||
8. If existing → update last_seen timestamp
|
||||
9. Persist registry to devices.json
|
||||
```
|
||||
|
||||
Manual add bypasses SSDP — takes IP and platform directly.
|
||||
|
||||
---
|
||||
|
||||
## Updating PROJECT_MAP.md
|
||||
|
||||
You MUST update PROJECT_MAP.md when you:
|
||||
|
||||
- Add a new source file
|
||||
- Change the directory structure
|
||||
- Implement a new CLI command or API endpoint
|
||||
- Change a data shape
|
||||
- Make a significant architectural decision
|
||||
- Complete a roadmap milestone
|
||||
|
||||
Update the "Last updated" date at the top of PROJECT_MAP.md with every change.
|
||||
|
||||
Do not let PROJECT_MAP.md drift from the actual codebase.
|
||||
|
||||
---
|
||||
|
||||
## Updating ROADMAP.md
|
||||
|
||||
When you complete a task:
|
||||
|
||||
1. Move it from "In Progress" or its milestone section to "Completed"
|
||||
2. Add the completion date
|
||||
3. Update the "Current Focus" section if the milestone changed
|
||||
|
||||
When you start a task:
|
||||
|
||||
1. Move it to "In Progress"
|
||||
2. Note what you're working on
|
||||
|
||||
---
|
||||
|
||||
## What To Do If You Are Unsure
|
||||
|
||||
If you are unsure about:
|
||||
|
||||
- **A design decision** → check PROJECT_MAP.md first. If not covered, ask.
|
||||
- **What to build next** → check ROADMAP.md current milestone.
|
||||
- **How a component should behave** → check README.md for user-facing behavior.
|
||||
- **Whether to add a feature** → default to no. Prefer smaller scope.
|
||||
|
||||
When in doubt, do less. A smaller correct implementation is better than
|
||||
a larger incorrect one. This project has a clear identity — do not add
|
||||
things that don't serve it.
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
|
||||
A feature is done when:
|
||||
|
||||
- [ ] It works correctly
|
||||
- [ ] `--json` output works if it's a CLI command
|
||||
- [ ] Errors include helpful `hint` text
|
||||
- [ ] Help text is complete (description, usage, examples, notes)
|
||||
- [ ] No `.unwrap()` in non-test paths
|
||||
- [ ] PROJECT_MAP.md is updated if structure changed
|
||||
- [ ] ROADMAP.md is updated to reflect completion
|
||||
Reference in New Issue
Block a user