diff --git a/.gitignore b/.gitignore
index f7e2abd..efbeb62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@ log
bin
db
build.sh
+.chess_history
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..ad157f0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,159 @@
+# Makefile for chess server and client
+
+# Variables
+BINARY_DIR := bin
+SERVER_BINARY := $(BINARY_DIR)/chess-server
+CLIENT_BINARY := $(BINARY_DIR)/chess-client
+SERVER_SOURCE := ./cmd/chess-server
+CLIENT_SOURCE := ./cmd/chess-client
+GO := go
+GOFLAGS := -trimpath
+LDFLAGS := -s -w
+
+# Build info
+GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
+BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
+VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null || echo "dev")
+
+# Default target
+.PHONY: all
+all: build
+
+# Build both binaries
+.PHONY: build
+build: server client
+
+# Build server only
+.PHONY: server
+server: $(SERVER_BINARY)
+
+$(SERVER_BINARY): $(BINARY_DIR)
+ $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(SERVER_BINARY) $(SERVER_SOURCE)
+ @echo "Built server: $(SERVER_BINARY)"
+
+# Build client only
+.PHONY: client
+client: $(CLIENT_BINARY)
+
+$(CLIENT_BINARY): $(BINARY_DIR)
+ $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(CLIENT_BINARY) $(CLIENT_SOURCE)
+ @echo "Built client: $(CLIENT_BINARY)"
+
+# Create bin directory
+$(BINARY_DIR):
+ @mkdir -p $(BINARY_DIR)
+
+# Run server with default settings
+.PHONY: run-server
+run-server: server
+ $(SERVER_BINARY) -api-port 8080 -dev -storage-path db/chess.db
+
+# Run server with web UI
+.PHONY: run-server-web
+run-server-web: server
+ $(SERVER_BINARY) -api-port 8080 -dev -storage-path db/chess.db -serve -web-port 9090
+
+# Run client
+.PHONY: run-client
+run-client: client
+ $(CLIENT_BINARY)
+
+# Run tests (start server and run test scripts)
+.PHONY: test
+test: server
+ test/run-test-server.sh
+
+# Run individual test suites
+.PHONY: test-api
+test-api:
+ test/test-api.sh
+
+.PHONY: test-db
+test-db:
+ test/test-db.sh
+
+.PHONY: test-longpoll
+test-longpoll:
+ test/test-longpoll.sh
+
+# Database operations
+.PHONY: db-init
+db-init: server
+ $(SERVER_BINARY) db init -path db/chess.db
+
+.PHONY: db-clean
+db-clean:
+ # ☣ DESTRUCTIVE: Removes database
+ rm -f db/chess.db db/chess.db-*
+
+# Development build (with race detector)
+.PHONY: dev
+dev:
+ $(GO) build -race -o $(SERVER_BINARY) $(SERVER_SOURCE)
+ $(GO) build -race -o $(CLIENT_BINARY) $(CLIENT_SOURCE)
+ @echo "Built with race detector enabled"
+
+# Clean build artifacts
+.PHONY: clean
+clean:
+ rm -f $(SERVER_BINARY) $(CLIENT_BINARY)
+ rm -rf $(BINARY_DIR)
+ @echo "Cleaned build artifacts"
+
+# Install dependencies
+.PHONY: deps
+deps:
+ $(GO) mod download
+ $(GO) mod verify
+
+# Update dependencies
+.PHONY: deps-update
+deps-update:
+ $(GO) get -u ./...
+ $(GO) mod tidy
+
+# Format code
+.PHONY: fmt
+fmt:
+ $(GO) fmt ./...
+
+# Run linter
+.PHONY: lint
+lint:
+ golangci-lint run ./...
+
+# Show help
+.PHONY: help
+help:
+ @echo "Chess Build System"
+ @echo ""
+ @echo "Build targets:"
+ @echo " make build Build both server and client"
+ @echo " make server Build server only"
+ @echo " make client Build client only"
+ @echo " make dev Build with race detector"
+ @echo ""
+ @echo "Run targets:"
+ @echo " make run-server Run server (port 8080, dev mode)"
+ @echo " make run-server-web Run server with web UI (ports 8080/9090)"
+ @echo " make run-client Run client"
+ @echo ""
+ @echo "Test targets:"
+ @echo " make test Run all tests"
+ @echo " make test-api Run API tests"
+ @echo " make test-db Run database tests"
+ @echo " make test-longpoll Run long-poll tests"
+ @echo ""
+ @echo "Database targets:"
+ @echo " make db-init Initialize database"
+ @echo " make db-clean Remove database (destructive)"
+ @echo ""
+ @echo "Maintenance:"
+ @echo " make clean Remove build artifacts"
+ @echo " make deps Download dependencies"
+ @echo " make deps-update Update dependencies"
+ @echo " make fmt Format code"
+ @echo " make lint Run linter"
+
+# Default make without arguments shows help
+.DEFAULT_GOAL := help
\ No newline at end of file
diff --git a/README.md b/README.md
index 2b8e772..b6bf7eb 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
Go Chess API
-
+
|
@@ -30,7 +30,7 @@ Go backend server providing a RESTful API for chess gameplay with user authentic
## Requirements
-- Go 1.24+
+- Go 1.25+
- Stockfish chess engine (`stockfish` in PATH)
- SQLite3 (for persistence features)
@@ -44,24 +44,45 @@ pkg install stockfish
```
## Quick Start
+
+### Using Make (Recommended)
+```bash
+git clone https://github.com/lixenwraith/chess
+cd chess
+make build
+
+# Standard mode with persistence and auth
+make run-server
+
+# Or run with web UI
+make run-server-web
+
+# Initialize database with user support
+make db-init
+
+# Add users via CLI
+./bin/chess-server db user add -path db/chess.db -username alice -password AlicePass123
+```
+
+### Building Manually
```bash
#git clone https://git.lixen.com/lixen/chess # Mirror
git clone https://github.com/lixenwraith/chess
cd chess
-go build ./cmd/chessd
+go build ./cmd/chess-server
# Standard mode with persistence and auth
-./chessd -storage-path chess.db
+./chess-server -storage-path chess.db
# Development mode with all features
-./chessd -dev -storage-path chess.db -pid /tmp/chessd.pid -pid-lock -port 9090
+./chess-server -dev -storage-path chess.db -pid /tmp/chess-server.pid -pid-lock -port 9090
# Initialize database with user support
-./chessd db init -path chess.db
+./chess-server db init -path chess.db
# Add users via CLI
-./chessd db user add -path chess.db -username alice -password AlicePass123
-./chessd db user list -path chess.db
+./chess-server db user add -path chess.db -username alice -password AlicePass123
+./chess-server db user list -path chess.db
```
Server listens on `http://localhost:8080`. See [API Reference](./doc/api.md) for endpoints including authentication.
@@ -73,28 +94,28 @@ The chess server supports user accounts with secure authentication:
### Creating Users
```bash
# Add user with password
-./chessd db user add -path chess.db -username alice -email alice@example.com -password SecurePass123
+./chess-server db user add -path chess.db -username alice -email alice@example.com -password SecurePass123
# Interactive password prompt
-./chessd db user add -path chess.db -username bob -interactive
+./chess-server db user add -path chess.db -username bob -interactive
# Import with existing hash
-./chessd db user add -path chess.db -username charlie -hash '$argon2id$...'
+./chess-server db user add -path chess.db -username charlie -hash '$argon2id$...'
```
### Managing Users
```bash
# List all users
-./chessd db user list -path chess.db
+./chess-server db user list -path chess.db
# Update password
-./chessd db user set-password -path chess.db -username alice -password NewPass456
+./chess-server db user set-password -path chess.db -username alice -password NewPass456
# Update email
-./chessd db user set-email -path chess.db -username alice -email newemail@example.com
+./chess-server db user set-email -path chess.db -username alice -email newemail@example.com
# Delete user
-./chessd db user delete -path chess.db -username alice
+./chess-server db user delete -path chess.db -username alice
```
## Web UI
@@ -104,13 +125,13 @@ The chess server includes an embedded web UI for playing games through a browser
### Enabling Web UI
```bash
# Start with web UI on default port 9090
-./chessd -serve
+./chess-server -serve
# Custom web UI port
-./chessd -serve -web-port 3000 -web-host 0.0.0.0
+./chess-server -serve -web-port 3000 -web-host 0.0.0.0
# Full example with authentication enabled
-./chessd -dev -serve -web-port 9090 -api-port 8080 -storage-path chess.db
+./chess-server -dev -serve -web-port 9090 -api-port 8080 -storage-path chess.db
```
### Features
@@ -130,6 +151,7 @@ Access the UI at `http://localhost:9090` when server is running with `-serve` fl
- [API Reference](./doc/api.md) - Endpoint specifications including auth
- [Architecture](./doc/architecture.md) - System design with auth layer
- [Development](./doc/development.md) - Build, test, and user management
+- [Client Guide](./doc/client.md) - Interactive debugging client
- [Stockfish Integration](./doc/stockfish.md) - Engine communication
## License
diff --git a/cmd/chess-client/main.go b/cmd/chess-client/main.go
new file mode 100644
index 0000000..c6a3eba
--- /dev/null
+++ b/cmd/chess-client/main.go
@@ -0,0 +1,139 @@
+// FILE: lixenwraith/chess/cmd/chess-client/main.go
+// Package main implements an interactive debugging client for the chess server API.
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "chess/internal/client/api"
+ "chess/internal/client/commands"
+ "chess/internal/client/display"
+ "chess/internal/client/session"
+
+ "github.com/chzyer/readline"
+)
+
+func main() {
+ s := &session.Session{
+ APIBaseURL: "http://localhost:8080",
+ Client: api.New("http://localhost:8080"),
+ Verbose: false,
+ }
+
+ // Initialize readline
+ rl, err := readline.NewEx(&readline.Config{
+ Prompt: display.Prompt("chess"),
+ HistoryFile: ".chess_history",
+ InterruptPrompt: "^C",
+ EOFPrompt: "exit",
+ })
+ if err != nil {
+ fmt.Printf("%s%s%s\n", display.Red, err.Error(), display.Reset)
+ os.Exit(1)
+ }
+ defer rl.Close()
+
+ fmt.Printf("%sChess Debug Client%s\n", display.Cyan, display.Reset)
+ fmt.Printf("%sAPI: %s%s\n", display.Cyan, s.APIBaseURL, display.Reset)
+ fmt.Printf("Type 'help' for commands\n\n")
+
+ registry := commands.NewRegistry(s)
+
+ for {
+ // Build enhanced prompt
+ prompt := buildPrompt(s)
+ rl.SetPrompt(prompt)
+
+ line, err := rl.Readline()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ continue
+ }
+
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+
+ if line == "exit" || line == "quit" || line == "x" {
+ break
+ }
+
+ // Check for verbose flag
+ if strings.HasSuffix(line, " -v") {
+ s.Verbose = true
+ line = strings.TrimSuffix(line, " -v")
+ } else {
+ s.Verbose = false
+ }
+
+ registry.Execute(line)
+ }
+}
+
+func buildPrompt(s *session.Session) string {
+ parts := []string{}
+
+ // Base
+ base := "chess"
+
+ // Add user/game context
+ if s.Username != "" {
+ parts = append(parts, fmt.Sprintf("%s%s%s", display.Magenta, s.Username, display.Reset))
+ }
+ if s.Username != "" && s.CurrentGame != "" {
+ parts = append(parts, fmt.Sprintf("%s - %s", display.Yellow, display.Reset))
+ }
+ if s.CurrentGame != "" {
+ parts = append(parts, fmt.Sprintf("%s%s%s", display.White, s.CurrentGame[:8], display.Reset))
+ }
+
+ // Add player color if in game
+ if s.CurrentGameState != nil && s.PlayerColor != "" {
+ colorText := ""
+ if s.PlayerColor == "w" {
+ colorText = display.Blue + "White" + display.Reset
+ } else {
+ colorText = display.Red + "Black" + display.Reset
+ }
+ parts = append(parts, colorText)
+ }
+
+ // Build first part
+ promptStr := base
+ if len(parts) > 0 {
+ promptStr += display.Yellow + " [" + display.Reset + strings.Join(parts, "") + display.Yellow + "]"
+ }
+
+ // Add game state if available
+ if s.CurrentGameState != nil {
+ turnInfo := ""
+ if s.CurrentGameState.Turn == "w" {
+ turnPlayer := "White"
+ playerType := "h"
+ if s.CurrentGameState.Players.White.Type == 2 {
+ playerType = "c"
+ }
+ turnInfo = fmt.Sprintf(" - Turn:%s(%s)",
+ fmt.Sprintf("%s%s%s", display.Blue, turnPlayer, display.Reset),
+ playerType)
+ } else {
+ turnPlayer := "Black"
+ playerType := "h"
+ if s.CurrentGameState.Players.Black.Type == 2 {
+ playerType = "c"
+ }
+ turnInfo = fmt.Sprintf(" - Turn:%s(%s)",
+ fmt.Sprintf("%s%s%s", display.Red, turnPlayer, display.Reset),
+ playerType)
+ }
+ promptStr += turnInfo
+ }
+
+ return display.Prompt(promptStr)
+}
\ No newline at end of file
diff --git a/cmd/chessd/cli/cli.go b/cmd/chess-server/cli/cli.go
similarity index 99%
rename from cmd/chessd/cli/cli.go
rename to cmd/chess-server/cli/cli.go
index 1bcf917..7ebf887 100644
--- a/cmd/chessd/cli/cli.go
+++ b/cmd/chess-server/cli/cli.go
@@ -1,4 +1,4 @@
-// FILE: cmd/chessd/cli/cli.go
+// FILE: lixenwraith/chess/cmd/chess-server/cli/cli.go
package cli
import (
@@ -10,7 +10,7 @@ import (
"text/tabwriter"
"time"
- "chess/internal/storage"
+ "chess/internal/server/storage"
"github.com/google/uuid"
"github.com/lixenwraith/auth"
diff --git a/cmd/chessd/main.go b/cmd/chess-server/main.go
similarity index 93%
rename from cmd/chessd/main.go
rename to cmd/chess-server/main.go
index 67f91fb..1293cd4 100644
--- a/cmd/chessd/main.go
+++ b/cmd/chess-server/main.go
@@ -1,4 +1,6 @@
-// FILE: cmd/chessd/main.go
+// FILE: lixenwraith/chess/cmd/chess-server/main.go
+// Package main implements the chess server application with RESTful API,
+// user authentication, and optional web UI serving capabilities.
package main
import (
@@ -12,12 +14,12 @@ import (
"syscall"
"time"
- "chess/cmd/chessd/cli"
- "chess/internal/http"
- "chess/internal/processor"
- "chess/internal/service"
- "chess/internal/storage"
- "chess/internal/webserver"
+ "chess/cmd/chess-server/cli"
+ "chess/internal/server/http"
+ "chess/internal/server/processor"
+ "chess/internal/server/service"
+ "chess/internal/server/storage"
+ "chess/internal/server/webserver"
)
const (
diff --git a/cmd/chessd/pid.go b/cmd/chess-server/pid.go
similarity index 96%
rename from cmd/chessd/pid.go
rename to cmd/chess-server/pid.go
index 54b659b..fa15533 100644
--- a/cmd/chessd/pid.go
+++ b/cmd/chess-server/pid.go
@@ -1,4 +1,4 @@
-// FILE: cmd/chessd/pid.go
+// FILE: lixenwraith/chess/cmd/chess-server/pid.go
package main
import (
@@ -10,8 +10,8 @@ import (
"syscall"
)
-// managePIDFile creates and manages a PID file with optional locking.
-// Returns a cleanup function that must be called on exit.
+// managePIDFile creates and manages a PID file with optional locking
+// Returns a cleanup function that must be called on exit
func managePIDFile(path string, lock bool) (func(), error) {
// Open/create PID file with exclusive create first attempt
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
diff --git a/doc/architecture.md b/doc/architecture.md
index 8a290b5..2c68003 100644
--- a/doc/architecture.md
+++ b/doc/architecture.md
@@ -29,6 +29,7 @@ SQLite persistence with async writes for games, synchronous writes for authentic
- **Board** (`internal/board`): FEN parsing and ASCII generation
- **Core** (`internal/core`): Shared types, API models, error constants
- **CLI** (`cmd/chessd/cli`): Database and user management commands
+- **Client** (`cmd/chess-client`, `internal/client`): Interactive debugging client with command registry, session management, and colored terminal output
## Request Flow
diff --git a/doc/client.md b/doc/client.md
new file mode 100644
index 0000000..803c67a
--- /dev/null
+++ b/doc/client.md
@@ -0,0 +1,291 @@
+# Chess Client Documentation
+
+The chess client is an interactive command-line debugging tool for the chess server API. It provides a rich terminal interface with colored output, command completion, and session management.
+
+## Features
+
+- Interactive shell with readline support and command history
+- Comprehensive command set for game operations and debugging
+- User authentication with JWT token management
+- Colored board visualization and game state display
+- Session persistence across commands
+- Verbose mode for detailed API request/response inspection
+- Long-polling support for real-time game updates
+
+## Building
+```bash
+go build ./cmd/chess-client
+```
+
+## Running
+```bash
+# Connect to default server (localhost:8080)
+./chess-client
+
+# The client starts with an interactive prompt
+chess >
+```
+
+### Prompt States
+
+The prompt dynamically updates to show context:
+- `chess` - Base prompt
+- `chess [username]` - Authenticated user
+- `chess [username - gameId]` - User in active game
+- `chess [username - gameId] White` - Shows player color
+- Turn indicator with player type (h=human, c=computer)
+
+## Command Reference
+
+### Authentication Commands
+
+#### `register` / `r`
+Register new user account.
+```
+chess > register
+Username: alice
+Password: ********
+Email (optional): alice@example.com
+```
+
+#### `login` / `l`
+Authenticate with existing account.
+```
+chess > login
+Username or Email: alice
+Password: ********
+```
+
+#### `logout` / `o`
+Clear authentication token.
+```
+chess > logout
+```
+
+#### `whoami` / `i`
+Display current authenticated user.
+```
+chess > whoami
+```
+
+#### `user` / `e`
+Set user ID manually (for display only, doesn't authenticate).
+```
+chess > user 550e8400-e29b-41d4-a716-446655440000
+```
+
+### Game Commands
+
+#### `new` / `n`
+Create new game interactively.
+```
+chess > new
+White player type (h/c) [h]: h
+Black player type (h/c) [h]: c
+Computer level (0-20) [10]: 15
+Search time (100-10000ms) [1000]: 2000
+Starting position (FEN) [default]:
+```
+
+#### `join` / `j`
+Set current game context.
+```
+chess > join a1b2c3d4-e5f6-7890-1234-567890abcdef
+```
+
+#### `move` / `m`
+Make chess move in UCI notation.
+```
+chess > move e2e4
+chess > move e7e5
+```
+
+#### `computer` / `c`
+Trigger computer move calculation.
+```
+chess > computer
+```
+
+#### `undo` / `u`
+Undo one or more moves.
+```
+chess > undo # Undo last move
+chess > undo 3 # Undo last 3 moves
+```
+
+#### `show` / `h`
+Display board and game state with colored pieces.
+```
+chess > show
+```
+
+#### `state` / `s`
+Show raw game JSON response.
+```
+chess > state
+```
+
+#### `delete` / `d`
+Delete game from server.
+```
+chess > delete # Delete current game
+chess > delete # Delete specific game
+```
+
+#### `poll` / `p`
+Long-poll for game updates (waits up to 25 seconds).
+```
+chess > poll
+```
+
+### Debug Commands
+
+#### `health` / `.`
+Check server health status.
+```
+chess > health
+```
+
+#### `url` / `/`
+Get or set API base URL.
+```
+chess > url # Show current URL
+chess > url http://localhost:9090 # Change server URL
+```
+
+#### `raw` / `:`
+Send raw API request.
+```
+chess > raw GET /api/v1/games/
+chess > raw POST /api/v1/games '{"white":{"type":1},"black":{"type":2}}'
+```
+
+#### `clear` / `-`
+Clear terminal screen.
+```
+chess > clear
+```
+
+#### `help` / `?`
+Display available commands or specific command usage.
+```
+chess > help # Show all commands
+chess > help move # Show move command details
+```
+
+#### `exit` / `x`
+Exit the client.
+```
+chess > exit
+```
+
+### Verbose Mode
+
+Append `-v` flag to any command for detailed output:
+```
+chess > move e2e4 -v
+```
+
+Shows full HTTP request/response with formatted JSON bodies.
+
+## Session Management
+
+The client maintains session state including:
+- API base URL
+- Current game ID
+- Authentication token
+- Username and user ID
+- Last move count (for polling)
+- Current game state
+- Player color assignment
+
+Session state persists across commands within the same client instance.
+
+## Display Features
+
+### Colored Output
+- **Blue**: White pieces, API requests, prompts
+- **Red**: Black pieces, errors
+- **Green**: Success messages
+- **Yellow**: Warnings, input prompts
+- **Magenta**: Computer moves, usernames
+- **Cyan**: Information, file coordinates
+- **White**: Game IDs
+
+### Board Visualization
+ASCII board with colored pieces:
+```
+ a b c d e f g h
+8 r n b q k b n r 8
+7 p p p p p p p p 7
+6 . . . . . . . . 6
+5 . . . . . . . . 5
+4 . . . . P . . . 4
+3 . . . . . . . . 3
+2 P P P P . P P P 2
+1 R N B Q K B N R 1
+ a b c d e f g h
+```
+
+### Move History
+Displayed in algebraic notation with move numbers:
+```
+History: 1.e4 e5 2.Nf3 Nc6 3.Bb5
+```
+
+## Workflows
+
+### Authenticated Game Creation
+1. Register or login to obtain token
+2. Create game (human players automatically associated with user)
+3. Make moves with authentication context
+4. Games persist with user association
+
+### Computer vs Computer Observation
+1. Create game with both players as computer
+2. Use polling to watch moves in real-time
+3. Board updates automatically as engines calculate
+
+### Debug Server Testing
+1. Set verbose mode for request inspection
+2. Use raw command for custom API calls
+3. Health endpoint for connectivity verification
+4. URL switching for multi-server testing
+
+## Configuration
+
+### Environment
+- History file: `.chess_history` in working directory
+- Default API URL: `http://localhost:8080`
+- Timeout: 30 seconds for HTTP requests
+
+### Limitations
+- No move validation in client (server validates)
+- No algebraic notation input (UCI format only)
+- Single game context at a time
+- No persistent authentication across restarts
+
+## Error Handling
+
+The client displays server errors with color coding:
+- Red text for error messages
+- Error codes for specific failures
+- Details when available
+
+Common error codes:
+- `GAME_NOT_FOUND` - Invalid game ID
+- `INVALID_MOVE` - Illegal chess move
+- `NOT_HUMAN_TURN` - Wrong player type
+- `GAME_OVER` - Game already ended
+- `RATE_LIMIT_EXCEEDED` - Too many requests
+
+## Development
+
+The client architecture follows a command pattern with:
+- **Registry**: Central command dispatcher
+- **Session**: State management interface
+- **API Client**: HTTP communication layer
+- **Display**: Terminal formatting utilities
+- **Commands**: Modular command handlers
+
+Extensions can add new commands by registering handlers in the appropriate command group (game, auth, debug).
\ No newline at end of file
diff --git a/doc/development.md b/doc/development.md
index 0167fa9..4fb7972 100644
--- a/doc/development.md
+++ b/doc/development.md
@@ -13,7 +13,8 @@
#git clone https://git.lixen.com/lixen/chess # Mirror
git clone https://github.com/lixenwraith/chess
cd chess
-go build ./cmd/chessd
+go build ./cmd/chess-server
+go build ./cmd/chess-client
```
## Running
@@ -118,30 +119,39 @@ go build ./cmd/chessd
## Project Structure
```
chess/
-├── cmd/chessd/
-│ ├── main.go # Entry point with auth initialization
-│ ├── pid.go # PID file management
-│ └── cli/ # Database and user CLI
+├── cmd/
+│ ├── chess-server/ # Server app
+│ │ ├── main.go # Server entry point
+│ │ ├── pid.go # PID file management
+│ │ └── cli/ # Database and user CLI
+│ └── chess-client/ # Client app
+│ └── main.go # Interactive debugging client
├── internal/
-│ ├── board/ # FEN/ASCII operations
-│ ├── core/ # Shared types and API models
-│ ├── engine/ # Stockfish UCI wrapper
-│ ├── game/ # Game state with player associations
-│ ├── http/ # Fiber handlers and auth endpoints
-│ │ ├── handler.go # Game endpoints
-│ │ ├── auth.go # Authentication endpoints
-│ │ └── middleware.go # JWT validation
-│ ├── processor/ # Command processing with user context
-│ ├── service/ # State and user management
-│ │ ├── service.go # Core service
-│ │ ├── game.go # Game operations
-│ │ └── user.go # User and auth operations
-│ └── storage/ # SQLite persistence
-│ ├── storage.go # Async writer for games
-│ ├── game.go # Game persistence
-│ ├── user.go # User persistence (synchronous)
-│ └── schema.go # Database schema
-└── test/ # Test scripts
+│ ├── client/ # Client components
+│ │ ├── api/ # HTTP client for server API
+│ │ ├── commands/ # Command registry and handlers
+│ │ ├── display/ # Terminal output formatting
+│ │ └── session/ # Session state management
+│ └── server/ # Server components
+│ ├── board/ # FEN/ASCII operations
+│ ├── core/ # Shared types and API models
+│ ├── engine/ # Stockfish UCI wrapper
+│ ├── game/ # Game state with player associations
+│ ├── http/ # Fiber handlers and auth endpoints
+│ │ ├── handler.go # Game endpoints
+│ │ ├── auth.go # Authentication endpoints
+│ │ └── middleware.go # JWT validation
+│ ├── processor/ # Command processing with user context
+│ ├── service/ # State and user management
+│ │ ├── service.go # Core service
+│ │ ├── game.go # Game operations
+│ │ └── user.go # User and auth operations
+│ └── storage/ # SQLite persistence
+│ ├── storage.go # Async writer for games
+│ ├── game.go # Game persistence
+│ ├── user.go # User persistence (synchronous)
+│ └── schema.go # Database schema
+└── test/ # Test scripts
```
## Testing
diff --git a/go.mod b/go.mod
index aaa08a7..b1203c7 100644
--- a/go.mod
+++ b/go.mod
@@ -1,14 +1,15 @@
module chess
-go 1.25.3
+go 1.25.4
require (
+ github.com/chzyer/readline v1.5.1
github.com/go-playground/validator/v10 v10.28.0
github.com/gofiber/fiber/v2 v2.52.9
github.com/google/uuid v1.6.0
github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226
github.com/mattn/go-sqlite3 v1.14.32
- golang.org/x/term v0.36.0
+ golang.org/x/term v0.37.0
)
require (
@@ -28,7 +29,7 @@ require (
github.com/tinylib/msgp v1.5.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.68.0 // indirect
- golang.org/x/crypto v0.43.0 // indirect
- golang.org/x/sys v0.37.0 // indirect
- golang.org/x/text v0.30.0 // indirect
+ golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/sys v0.38.0 // indirect
+ golang.org/x/text v0.31.0 // indirect
)
diff --git a/go.sum b/go.sum
index b870437..234b84c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,11 @@
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
+github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
+github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
+github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
+github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
+github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
@@ -50,14 +56,15 @@ github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFn
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
-golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
-golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
+golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
-golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
-golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
-golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
+golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/client/api/client.go b/internal/client/api/client.go
new file mode 100644
index 0000000..58bf0f0
--- /dev/null
+++ b/internal/client/api/client.go
@@ -0,0 +1,241 @@
+// FILE: lixenwraith/chess/internal/api/client.go
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "chess/internal/client/display"
+)
+
+type Client struct {
+ BaseURL string
+ AuthToken string
+ HTTPClient *http.Client
+ Verbose bool
+}
+
+func New(baseURL string) *Client {
+ return &Client{
+ BaseURL: baseURL,
+ HTTPClient: &http.Client{
+ Timeout: 30 * time.Second,
+ },
+ }
+}
+
+func (c *Client) SetVerbose(v bool) {
+ c.Verbose = v
+}
+
+// SetBaseURL updates the API base URL for the client
+func (c *Client) SetBaseURL(url string) {
+ c.BaseURL = strings.TrimRight(url, "/")
+}
+
+func (c *Client) SetToken(token string) {
+ c.AuthToken = token
+}
+
+func (c *Client) doRequest(method, path string, body interface{}, result interface{}) error {
+ url := c.BaseURL + path
+
+ // Prepare body
+ var bodyReader io.Reader
+ var bodyStr string
+ if body != nil {
+ jsonData, err := json.Marshal(body)
+ if err != nil {
+ return err
+ }
+ bodyReader = bytes.NewReader(jsonData)
+ bodyStr = string(jsonData)
+ }
+
+ // Create request
+ req, err := http.NewRequest(method, url, bodyReader)
+ if err != nil {
+ return err
+ }
+
+ // Set headers
+ if body != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+ if c.AuthToken != "" {
+ req.Header.Set("Authorization", "Bearer "+c.AuthToken)
+ }
+
+ // Display request
+ fmt.Printf("\n%s[API] %s %s%s\n", display.Blue, method, path, display.Reset)
+ if bodyStr != "" {
+ if c.Verbose {
+ // Display request body if verbose
+ var prettyBody interface{}
+ json.Unmarshal([]byte(bodyStr), &prettyBody)
+ prettyJSON, _ := json.MarshalIndent(prettyBody, "", " ")
+ fmt.Printf("%sRequest Body:%s\n%s\n", display.Cyan, display.Reset, string(prettyJSON))
+ } else {
+ fmt.Printf("%s%s%s\n", display.Blue, bodyStr, display.Reset)
+ }
+ }
+
+ // Execute request
+ resp, err := c.HTTPClient.Do(req)
+ if err != nil {
+ fmt.Printf("%s[ERROR] %s%s\n", display.Red, err.Error(), display.Reset)
+ return err
+ }
+ defer resp.Body.Close()
+
+ // Read response
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ // Display response
+ statusColor := display.Green
+ if resp.StatusCode >= 400 {
+ statusColor = display.Red
+ }
+ fmt.Printf("%s[%d %s]%s\n", statusColor, resp.StatusCode, http.StatusText(resp.StatusCode), display.Reset)
+
+ // Display response body if verbose
+ if c.Verbose && len(respBody) > 0 {
+ var prettyResp interface{}
+ if err := json.Unmarshal(respBody, &prettyResp); err == nil {
+ prettyJSON, _ := json.MarshalIndent(prettyResp, "", " ")
+ fmt.Printf("%sResponse Body:%s\n%s\n", display.Cyan, display.Reset, string(prettyJSON))
+ } else {
+ fmt.Printf("%sResponse:%s\n%s\n", display.Cyan, display.Reset, string(respBody))
+ }
+ }
+
+ // Parse error response
+ if resp.StatusCode >= 400 {
+ var errResp ErrorResponse
+ if err := json.Unmarshal(respBody, &errResp); err == nil {
+ if !c.Verbose {
+ fmt.Printf("%sError: %s%s\n", display.Red, errResp.Error, display.Reset)
+ if errResp.Code != "" {
+ fmt.Printf("%sCode: %s%s\n", display.Red, errResp.Code, display.Reset)
+ }
+ if errResp.Details != "" {
+ fmt.Printf("%sDetails: %s%s\n", display.Red, errResp.Details, display.Reset)
+ }
+ }
+ } else if !c.Verbose {
+ fmt.Printf("%s%s%s\n", display.Red, string(respBody), display.Reset)
+ }
+ return fmt.Errorf("request failed with status %d", resp.StatusCode)
+ }
+
+ // Parse success response
+ if result != nil && len(respBody) > 0 {
+ if err := json.Unmarshal(respBody, result); err != nil {
+ // For debug, show raw response if parsing fails
+ fmt.Printf("%sResponse parse error: %s%s\n", display.Red, err.Error(), display.Reset)
+ fmt.Printf("%sRaw response: %s%s\n", display.Green, string(respBody), display.Reset)
+ return err
+ }
+ }
+
+ return nil
+}
+
+// API Methods
+
+func (c *Client) Health() (*HealthResponse, error) {
+ var resp HealthResponse
+ err := c.doRequest("GET", "/health", nil, &resp)
+ return &resp, err
+}
+
+func (c *Client) CreateGame(req *CreateGameRequest) (*GameResponse, error) {
+ var resp GameResponse
+ err := c.doRequest("POST", "/api/v1/games", req, &resp)
+ return &resp, err
+}
+
+func (c *Client) GetGame(gameID string) (*GameResponse, error) {
+ var resp GameResponse
+ err := c.doRequest("GET", "/api/v1/games/"+gameID, nil, &resp)
+ return &resp, err
+}
+
+func (c *Client) GetGameWithPoll(gameID string, moveCount int) (*GameResponse, error) {
+ var resp GameResponse
+ path := fmt.Sprintf("/api/v1/games/%s?wait=true&moveCount=%d", gameID, moveCount)
+ err := c.doRequest("GET", path, nil, &resp)
+ return &resp, err
+}
+
+func (c *Client) DeleteGame(gameID string) error {
+ return c.doRequest("DELETE", "/api/v1/games/"+gameID, nil, nil)
+}
+
+func (c *Client) MakeMove(gameID string, move string) (*GameResponse, error) {
+ req := &MoveRequest{Move: move}
+ var resp GameResponse
+ err := c.doRequest("POST", "/api/v1/games/"+gameID+"/moves", req, &resp)
+ return &resp, err
+}
+
+func (c *Client) UndoMoves(gameID string, count int) (*GameResponse, error) {
+ req := &UndoRequest{Count: count}
+ var resp GameResponse
+ err := c.doRequest("POST", "/api/v1/games/"+gameID+"/undo", req, &resp)
+ return &resp, err
+}
+
+func (c *Client) GetBoard(gameID string) (*BoardResponse, error) {
+ var resp BoardResponse
+ err := c.doRequest("GET", "/api/v1/games/"+gameID+"/board", nil, &resp)
+ return &resp, err
+}
+
+func (c *Client) Register(username, password, email string) (*AuthResponse, error) {
+ req := &RegisterRequest{
+ Username: username,
+ Password: password,
+ Email: email,
+ }
+ var resp AuthResponse
+ err := c.doRequest("POST", "/api/v1/auth/register", req, &resp)
+ return &resp, err
+}
+
+func (c *Client) Login(identifier, password string) (*AuthResponse, error) {
+ req := &LoginRequest{
+ Identifier: identifier,
+ Password: password,
+ }
+ var resp AuthResponse
+ err := c.doRequest("POST", "/api/v1/auth/login", req, &resp)
+ return &resp, err
+}
+
+func (c *Client) GetCurrentUser() (*UserResponse, error) {
+ var resp UserResponse
+ err := c.doRequest("GET", "/api/v1/auth/me", nil, &resp)
+ return &resp, err
+}
+
+// RawRequest performs a raw HTTP request for debugging purposes
+func (c *Client) RawRequest(method, path string, body string) error {
+ var bodyData interface{}
+ if body != "" {
+ if err := json.Unmarshal([]byte(body), &bodyData); err != nil {
+ // Try as raw string
+ bodyData = body
+ }
+ }
+
+ return c.doRequest(method, path, bodyData, nil)
+}
\ No newline at end of file
diff --git a/internal/client/api/types.go b/internal/client/api/types.go
new file mode 100644
index 0000000..1d230db
--- /dev/null
+++ b/internal/client/api/types.go
@@ -0,0 +1,97 @@
+// FILE: lixenwraith/chess/internal/client/api/types.go
+package api
+
+import "time"
+
+// Request types
+type CreateGameRequest struct {
+ White PlayerConfig `json:"white"`
+ Black PlayerConfig `json:"black"`
+ FEN string `json:"fen,omitempty"`
+}
+
+type PlayerConfig struct {
+ Type int `json:"type"` // 1=human, 2=computer
+ Level int `json:"level,omitempty"`
+ SearchTime int `json:"searchTime,omitempty"`
+}
+
+type MoveRequest struct {
+ Move string `json:"move"`
+}
+
+type UndoRequest struct {
+ Count int `json:"count"`
+}
+
+type RegisterRequest struct {
+ Username string `json:"username"`
+ Email string `json:"email,omitempty"`
+ Password string `json:"password"`
+}
+
+type LoginRequest struct {
+ Identifier string `json:"identifier"`
+ Password string `json:"password"`
+}
+
+// Response types
+type GameResponse struct {
+ GameID string `json:"gameId"`
+ FEN string `json:"fen"`
+ Turn string `json:"turn"`
+ State string `json:"state"`
+ Moves []string `json:"moves"`
+ Players PlayersResponse `json:"players"`
+ LastMove *MoveInfo `json:"lastMove,omitempty"`
+}
+
+type PlayersResponse struct {
+ White PlayerInfo `json:"white"`
+ Black PlayerInfo `json:"black"`
+}
+
+type PlayerInfo struct {
+ ID string `json:"id"`
+ Type int `json:"type"`
+ Level int `json:"level,omitempty"`
+ SearchTime int `json:"searchTime,omitempty"`
+}
+
+type MoveInfo struct {
+ Move string `json:"move"`
+ PlayerColor string `json:"playerColor"`
+ Score int `json:"score,omitempty"`
+ Depth int `json:"depth,omitempty"`
+}
+
+type BoardResponse struct {
+ FEN string `json:"fen"`
+ Board string `json:"board"`
+}
+
+type AuthResponse struct {
+ Token string `json:"token"`
+ UserID string `json:"userId"`
+ Username string `json:"username"`
+}
+
+type UserResponse struct {
+ UserID string `json:"userId"`
+ Username string `json:"username"`
+ Email string `json:"email,omitempty"`
+ CreatedAt time.Time `json:"createdAt"`
+ LastLogin *time.Time `json:"lastLoginAt,omitempty"`
+}
+
+type ErrorResponse struct {
+ Error string `json:"error"`
+ Code string `json:"code"`
+ Details string `json:"details,omitempty"`
+}
+
+type HealthResponse struct {
+ Status string `json:"status"`
+ Time int64 `json:"time"`
+ Storage string `json:"storage,omitempty"`
+}
\ No newline at end of file
diff --git a/internal/client/commands/auth.go b/internal/client/commands/auth.go
new file mode 100644
index 0000000..0d1aa8e
--- /dev/null
+++ b/internal/client/commands/auth.go
@@ -0,0 +1,181 @@
+// FILE: lixenwraith/chess/internal/client/commands/auth.go
+package commands
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+ "syscall"
+
+ "chess/internal/client/api"
+ "chess/internal/client/display"
+
+ "golang.org/x/term"
+)
+
+func (r *Registry) registerAuthCommands() {
+ r.Register(&Command{
+ Name: "register",
+ ShortName: "r",
+ Description: "Register a new user",
+ Usage: "register",
+ Handler: registerHandler,
+ })
+
+ r.Register(&Command{
+ Name: "login",
+ ShortName: "l",
+ Description: "Login with credentials",
+ Usage: "login",
+ Handler: loginHandler,
+ })
+
+ r.Register(&Command{
+ Name: "logout",
+ ShortName: "o",
+ Description: "Clear authentication",
+ Usage: "logout",
+ Handler: logoutHandler,
+ })
+
+ r.Register(&Command{
+ Name: "whoami",
+ ShortName: "i",
+ Description: "Show current user",
+ Usage: "whoami",
+ Handler: whoamiHandler,
+ })
+
+ r.Register(&Command{
+ Name: "user",
+ ShortName: "e",
+ Description: "Set user ID manually",
+ Usage: "user ",
+ Handler: setUserHandler,
+ })
+}
+
+func readPassword(prompt string) (string, error) {
+ fmt.Print(prompt)
+ bytePassword, err := term.ReadPassword(int(syscall.Stdin))
+ fmt.Println()
+ if err != nil {
+ return "", err
+ }
+ return string(bytePassword), nil
+}
+
+func registerHandler(s Session, args []string) error {
+ scanner := bufio.NewScanner(os.Stdin)
+ c := s.GetClient().(*api.Client)
+
+ fmt.Print(display.Yellow + "Username: " + display.Reset)
+ scanner.Scan()
+ username := strings.TrimSpace(scanner.Text())
+
+ password, err := readPassword(display.Yellow + "Password: " + display.Reset)
+ if err != nil {
+ return err
+ }
+
+ fmt.Print(display.Yellow + "Email (optional): " + display.Reset)
+ scanner.Scan()
+ email := strings.TrimSpace(scanner.Text())
+
+ resp, err := c.Register(username, password, email)
+ if err != nil {
+ return err
+ }
+
+ s.SetAuthToken(resp.Token)
+ s.SetCurrentUser(resp.UserID)
+ s.SetUsername(resp.Username)
+ c.SetToken(resp.Token)
+
+ fmt.Printf("%sRegistered successfully%s\n", display.Green, display.Reset)
+ fmt.Printf("User ID: %s\n", resp.UserID)
+ fmt.Printf("Username: %s\n", resp.Username)
+
+ return nil
+}
+
+func loginHandler(s Session, args []string) error {
+ scanner := bufio.NewScanner(os.Stdin)
+ c := s.GetClient().(*api.Client)
+
+ fmt.Print(display.Yellow + "Username or Email: " + display.Reset)
+ scanner.Scan()
+ identifier := strings.TrimSpace(scanner.Text())
+
+ password, err := readPassword(display.Yellow + "Password: " + display.Reset)
+ if err != nil {
+ return err
+ }
+
+ resp, err := c.Login(identifier, password)
+ if err != nil {
+ return err
+ }
+
+ s.SetAuthToken(resp.Token)
+ s.SetCurrentUser(resp.UserID)
+ s.SetUsername(resp.Username)
+ c.SetToken(resp.Token)
+
+ fmt.Printf("%sLogged in successfully%s\n", display.Green, display.Reset)
+ fmt.Printf("User ID: %s\n", resp.UserID)
+ fmt.Printf("Username: %s\n", resp.Username)
+
+ return nil
+}
+
+func logoutHandler(s Session, args []string) error {
+ s.SetAuthToken("")
+ s.SetCurrentUser("")
+ s.SetUsername("")
+ c := s.GetClient().(*api.Client)
+ c.SetToken("")
+
+ fmt.Printf("%sLogged out%s\n", display.Green, display.Reset)
+ return nil
+}
+
+func whoamiHandler(s Session, args []string) error {
+ if s.GetAuthToken() == "" {
+ fmt.Printf("%sNot authenticated%s\n", display.Yellow, display.Reset)
+ return nil
+ }
+
+ c := s.GetClient().(*api.Client)
+ user, err := c.GetCurrentUser()
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("%sCurrent User:%s\n", display.Cyan, display.Reset)
+ fmt.Printf(" User ID: %s\n", user.UserID)
+ fmt.Printf(" Username: %s\n", user.Username)
+ if user.Email != "" {
+ fmt.Printf(" Email: %s\n", user.Email)
+ }
+ fmt.Printf(" Created: %s\n", user.CreatedAt.Format("2006-01-02 15:04:05"))
+ if user.LastLogin != nil {
+ fmt.Printf(" Last Login: %s\n", user.LastLogin.Format("2006-01-02 15:04:05"))
+ }
+
+ return nil
+}
+
+func setUserHandler(s Session, args []string) error {
+ if len(args) < 1 {
+ return fmt.Errorf("usage: user ")
+ }
+
+ userID := args[0]
+ s.SetCurrentUser(userID)
+ fmt.Printf("%sUser ID set to: %s%s\n", display.Cyan, userID, display.Reset)
+ fmt.Println("Note: This doesn't authenticate, just sets the ID for display")
+
+ return nil
+}
\ No newline at end of file
diff --git a/internal/client/commands/debug.go b/internal/client/commands/debug.go
new file mode 100644
index 0000000..b3bfb84
--- /dev/null
+++ b/internal/client/commands/debug.go
@@ -0,0 +1,108 @@
+// FILE: lixenwraith/chess/internal/client/commands/debug.go
+package commands
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "chess/internal/client/api"
+ "chess/internal/client/display"
+)
+
+func (r *Registry) registerDebugCommands() {
+ r.Register(&Command{
+ Name: "health",
+ ShortName: ".",
+ Description: "Check server health",
+ Usage: "health",
+ Handler: healthHandler,
+ })
+
+ r.Register(&Command{
+ Name: "url",
+ ShortName: "/",
+ Description: "Set API base URL",
+ Usage: "url [apiUrl]",
+ Handler: urlHandler,
+ })
+
+ r.Register(&Command{
+ Name: "raw",
+ ShortName: ":",
+ Description: "Send raw API request",
+ Usage: "raw [json-body]",
+ Handler: rawRequestHandler,
+ })
+
+ r.Register(&Command{
+ Name: "clear",
+ ShortName: "-",
+ Description: "Clear screen",
+ Usage: "clear",
+ Handler: clearHandler,
+ })
+}
+
+func healthHandler(s Session, args []string) error {
+ c := s.GetClient().(*api.Client)
+ resp, err := c.Health()
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("%sServer Health:%s\n", display.Cyan, display.Reset)
+ fmt.Printf(" Status: %s\n", resp.Status)
+ // Convert Unix timestamp to readable time
+ t := time.Unix(resp.Time, 0)
+ fmt.Printf(" Time: %s\n", t.Format("2006-01-02 15:04:05"))
+ if resp.Storage != "" {
+ fmt.Printf(" Storage: %s\n", resp.Storage)
+ }
+
+ return nil
+}
+
+func urlHandler(s Session, args []string) error {
+ if len(args) == 0 {
+ fmt.Printf("Current API URL: %s\n", s.GetAPIBaseURL())
+ return nil
+ }
+
+ url := args[0]
+ if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
+ url = "http://" + url
+ }
+
+ s.SetAPIBaseURL(url)
+ c := s.GetClient().(*api.Client)
+ c.SetBaseURL(url)
+
+ fmt.Printf("%sAPI URL set to: %s%s\n", display.Cyan, url, display.Reset)
+ return nil
+}
+
+func rawRequestHandler(s Session, args []string) error {
+ if len(args) < 2 {
+ return fmt.Errorf("usage: raw [json-body]")
+ }
+
+ method := strings.ToUpper(args[0])
+ path := args[1]
+
+ body := ""
+ if len(args) > 2 {
+ body = strings.Join(args[2:], " ")
+ }
+
+ c := s.GetClient().(*api.Client)
+ return c.RawRequest(method, path, body)
+}
+
+func clearHandler(s Session, args []string) error {
+ cmd := exec.Command("clear")
+ cmd.Stdout = os.Stdout
+ return cmd.Run()
+}
\ No newline at end of file
diff --git a/internal/client/commands/game.go b/internal/client/commands/game.go
new file mode 100644
index 0000000..2eb4b5f
--- /dev/null
+++ b/internal/client/commands/game.go
@@ -0,0 +1,511 @@
+// FILE: lixenwraith/chess/internal/client/commands/game.go
+package commands
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "chess/internal/client/api"
+ "chess/internal/client/display"
+)
+
+func (r *Registry) registerGameCommands() {
+ r.Register(&Command{
+ Name: "new",
+ ShortName: "n",
+ Description: "Create a new game",
+ Usage: "new",
+ Handler: newGameHandler,
+ })
+
+ r.Register(&Command{
+ Name: "join",
+ ShortName: "j",
+ Description: "Join/set current game ID",
+ Usage: "join ",
+ Handler: joinGameHandler,
+ })
+
+ r.Register(&Command{
+ Name: "move",
+ ShortName: "m",
+ Description: "Make a move",
+ Usage: "move ",
+ Handler: moveHandler,
+ })
+
+ r.Register(&Command{
+ Name: "computer",
+ ShortName: "c",
+ Description: "Trigger computer move",
+ Usage: "computer",
+ Handler: computerMoveHandler,
+ })
+
+ r.Register(&Command{
+ Name: "undo",
+ ShortName: "u",
+ Description: "Undo moves",
+ Usage: "undo [count]",
+ Handler: undoHandler,
+ })
+
+ r.Register(&Command{
+ Name: "show",
+ ShortName: "h",
+ Description: "Show board and game state",
+ Usage: "show",
+ Handler: showBoardHandler,
+ })
+
+ r.Register(&Command{
+ Name: "state",
+ ShortName: "s",
+ Description: "Show raw game JSON",
+ Usage: "state",
+ Handler: gameStateHandler,
+ })
+
+ r.Register(&Command{
+ Name: "delete",
+ ShortName: "d",
+ Description: "Delete a game",
+ Usage: "delete [gameId]",
+ Handler: deleteGameHandler,
+ })
+
+ r.Register(&Command{
+ Name: "poll",
+ ShortName: "p",
+ Description: "Long-poll for game updates",
+ Usage: "poll",
+ Handler: pollHandler,
+ })
+}
+
+func newGameHandler(s Session, args []string) error {
+ scanner := bufio.NewScanner(os.Stdin)
+ c := s.GetClient().(*api.Client)
+
+ fmt.Println("\n" + display.Cyan + "Creating new game..." + display.Reset)
+
+ // White player
+ fmt.Print(display.Yellow + "White player type (h/c) [h]: " + display.Reset)
+ scanner.Scan()
+ whiteType := strings.ToLower(strings.TrimSpace(scanner.Text()))
+ if whiteType == "" {
+ whiteType = "h"
+ }
+
+ white := api.PlayerConfig{Type: 1}
+ if whiteType == "c" {
+ white.Type = 2
+
+ fmt.Print(display.Yellow + "Computer level (0-20) [10]: " + display.Reset)
+ scanner.Scan()
+ levelStr := strings.TrimSpace(scanner.Text())
+ if levelStr == "" {
+ white.Level = 10
+ } else {
+ level, _ := strconv.Atoi(levelStr)
+ white.Level = level
+ }
+
+ fmt.Print(display.Yellow + "Search time (100-10000ms) [1000]: " + display.Reset)
+ scanner.Scan()
+ timeStr := strings.TrimSpace(scanner.Text())
+ if timeStr == "" {
+ white.SearchTime = 1000
+ } else {
+ searchTime, _ := strconv.Atoi(timeStr)
+ white.SearchTime = searchTime
+ }
+ }
+
+ // Black player
+ fmt.Print(display.Yellow + "Black player type (h/c) [h]: " + display.Reset)
+ scanner.Scan()
+ blackType := strings.ToLower(strings.TrimSpace(scanner.Text()))
+ if blackType == "" {
+ blackType = "h"
+ }
+
+ black := api.PlayerConfig{Type: 1}
+ if blackType == "c" {
+ black.Type = 2
+
+ fmt.Print(display.Yellow + "Computer level (0-20) [10]: " + display.Reset)
+ scanner.Scan()
+ levelStr := strings.TrimSpace(scanner.Text())
+ if levelStr == "" {
+ black.Level = 10
+ } else {
+ level, _ := strconv.Atoi(levelStr)
+ black.Level = level
+ }
+
+ fmt.Print(display.Yellow + "Search time (100-10000ms) [1000]: " + display.Reset)
+ scanner.Scan()
+ timeStr := strings.TrimSpace(scanner.Text())
+ if timeStr == "" {
+ black.SearchTime = 1000
+ } else {
+ searchTime, _ := strconv.Atoi(timeStr)
+ black.SearchTime = searchTime
+ }
+ }
+
+ // Starting position
+ fmt.Print(display.Yellow + "Starting position (FEN) [default]: " + display.Reset)
+ scanner.Scan()
+ fen := strings.TrimSpace(scanner.Text())
+
+ req := &api.CreateGameRequest{
+ White: white,
+ Black: black,
+ FEN: fen,
+ }
+
+ resp, err := c.CreateGame(req)
+ if err != nil {
+ return err
+ }
+
+ s.SetCurrentGame(resp.GameID)
+ s.SetLastMoveCount(len(resp.Moves))
+ s.SetGameState(resp)
+
+ // Determine player color if authenticated
+ if s.GetCurrentUser() != "" {
+ if resp.Players.White.ID == s.GetCurrentUser() {
+ s.SetPlayerColor("w")
+ } else if resp.Players.Black.ID == s.GetCurrentUser() {
+ s.SetPlayerColor("b")
+ }
+ }
+
+ fmt.Printf("%sGame created: %s%s\n", display.Green, resp.GameID, display.Reset)
+ fmt.Printf("%sCurrent game set to: %s%s\n", display.Cyan, resp.GameID, display.Reset)
+
+ // If white is computer, trigger first move
+ if white.Type == 2 {
+ fmt.Printf("\n%sTriggering white computer move...%s\n", display.Magenta, display.Reset)
+ time.Sleep(100 * time.Millisecond)
+ _, err = c.MakeMove(resp.GameID, "cccc")
+ if err != nil {
+ fmt.Printf("%sFailed to trigger computer move: %s%s\n", display.Red, err.Error(), display.Reset)
+ }
+ }
+
+ return nil
+}
+
+func joinGameHandler(s Session, args []string) error {
+ if len(args) < 1 {
+ return fmt.Errorf("usage: join ")
+ }
+
+ gameID := args[0]
+ c := s.GetClient().(*api.Client)
+
+ // Verify game exists
+ resp, err := c.GetGame(gameID)
+ if err != nil {
+ return err
+ }
+
+ s.SetCurrentGame(gameID)
+ s.SetLastMoveCount(len(resp.Moves))
+ s.SetGameState(resp)
+
+ // Determine player color if authenticated
+ if s.GetCurrentUser() != "" {
+ if resp.Players.White.ID == s.GetCurrentUser() {
+ s.SetPlayerColor("w")
+ } else if resp.Players.Black.ID == s.GetCurrentUser() {
+ s.SetPlayerColor("b")
+ } else {
+ s.SetPlayerColor("")
+ }
+ }
+
+ fmt.Printf("%sJoined game: %s%s\n", display.Green, gameID, display.Reset)
+ fmt.Printf("Turn: %s | State: %s | Moves: %d\n", resp.Turn, resp.State, len(resp.Moves))
+
+ return nil
+}
+
+func moveHandler(s Session, args []string) error {
+ if len(args) < 1 {
+ return fmt.Errorf("usage: move ")
+ }
+
+ gameID := s.GetCurrentGame()
+ if gameID == "" {
+ return fmt.Errorf("no current game, use 'new' or 'join '")
+ }
+
+ move := args[0]
+ c := s.GetClient().(*api.Client)
+
+ resp, err := c.MakeMove(gameID, move)
+ if err != nil {
+ return err
+ }
+
+ s.SetLastMoveCount(len(resp.Moves))
+ s.SetGameState(resp)
+ fmt.Printf("%sMove accepted%s\n", display.Green, display.Reset)
+
+ // Check if computer should move
+ currentTurn := resp.Turn
+ var computerPlayer *api.PlayerInfo
+ if currentTurn == "w" && resp.Players.White.Type == 2 {
+ computerPlayer = &resp.Players.White
+ } else if currentTurn == "b" && resp.Players.Black.Type == 2 {
+ computerPlayer = &resp.Players.Black
+ }
+
+ if computerPlayer != nil && resp.State == "ongoing" {
+ fmt.Printf("\n%sComputer's turn, triggering move...%s\n", display.Magenta, display.Reset)
+ time.Sleep(100 * time.Millisecond)
+ resp2, err := c.MakeMove(gameID, "cccc")
+ if err != nil {
+ fmt.Printf("%sFailed to trigger computer move: %s%s\n", display.Red, err.Error(), display.Reset)
+ } else if resp2.State == "pending" {
+ fmt.Printf("%sComputer is thinking...%s\n", display.Magenta, display.Reset)
+ // Wait for completion
+ for i := 0; i < 50; i++ {
+ time.Sleep(200 * time.Millisecond)
+ resp3, err := c.GetGame(gameID)
+ if err == nil && resp3.State != "pending" {
+ s.SetLastMoveCount(len(resp3.Moves))
+ if resp3.LastMove != nil {
+ fmt.Printf("%sComputer played: %s%s", display.Magenta, resp3.LastMove.Move, display.Reset)
+ if resp3.LastMove.Depth > 0 {
+ fmt.Printf(" (depth %d, score %d)", resp3.LastMove.Depth, resp3.LastMove.Score)
+ }
+ fmt.Println()
+ }
+ break
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func computerMoveHandler(s Session, args []string) error {
+ gameID := s.GetCurrentGame()
+ if gameID == "" {
+ return fmt.Errorf("no current game, use 'new' or 'join '")
+ }
+
+ c := s.GetClient().(*api.Client)
+
+ resp, err := c.MakeMove(gameID, "cccc")
+ if err != nil {
+ return err
+ }
+
+ if resp.State == "pending" {
+ fmt.Printf("%sComputer is thinking...%s\n", display.Magenta, display.Reset)
+
+ // Poll for completion
+ for i := 0; i < 50; i++ {
+ time.Sleep(200 * time.Millisecond)
+ resp2, err := c.GetGame(gameID)
+ if err == nil && resp2.State != "pending" {
+ s.SetLastMoveCount(len(resp2.Moves))
+ s.SetGameState(resp2)
+ if resp2.LastMove != nil {
+ fmt.Printf("%sComputer played: %s%s", display.Magenta, resp2.LastMove.Move, display.Reset)
+ if resp2.LastMove.Depth > 0 {
+ fmt.Printf(" (depth %d, score %d)", resp2.LastMove.Depth, resp2.LastMove.Score)
+ }
+ fmt.Println()
+ }
+ return nil
+ }
+ }
+ return fmt.Errorf("timeout waiting for computer move")
+ }
+
+ s.SetLastMoveCount(len(resp.Moves))
+ fmt.Printf("%sMove triggered%s\n", display.Green, display.Reset)
+ return nil
+}
+
+func undoHandler(s Session, args []string) error {
+ gameID := s.GetCurrentGame()
+ if gameID == "" {
+ return fmt.Errorf("no current game, use 'new' or 'join '")
+ }
+
+ count := 1
+ if len(args) > 0 {
+ var err error
+ count, err = strconv.Atoi(args[0])
+ if err != nil {
+ return fmt.Errorf("invalid count: %s", args[0])
+ }
+ }
+
+ c := s.GetClient().(*api.Client)
+ resp, err := c.UndoMoves(gameID, count)
+ if err != nil {
+ return err
+ }
+
+ s.SetLastMoveCount(len(resp.Moves))
+ fmt.Printf("%sUndid %d move(s)%s\n", display.Green, count, display.Reset)
+ return nil
+}
+
+func showBoardHandler(s Session, args []string) error {
+ gameID := s.GetCurrentGame()
+ if gameID == "" {
+ return fmt.Errorf("no current game, use 'new' or 'join '")
+ }
+
+ c := s.GetClient().(*api.Client)
+
+ // Get full game state
+ game, err := c.GetGame(gameID)
+ if err != nil {
+ return err
+ }
+
+ // Get ASCII board
+ board, err := c.GetBoard(gameID)
+ if err != nil {
+ return err
+ }
+
+ s.SetLastMoveCount(len(game.Moves))
+ s.SetGameState(game)
+
+ // Display board with colors
+ fmt.Println()
+ display.RenderBoard(board.Board)
+
+ // Display game info
+ fmt.Printf("\nFEN: %s\n", game.FEN)
+ fmt.Printf("Turn: %s | State: %s | Moves: %d\n",
+ display.ColorForTurn(game.Turn), game.State, len(game.Moves))
+
+ // Display move history
+ if len(game.Moves) > 0 {
+ fmt.Printf("\nHistory: ")
+ for i, move := range game.Moves {
+ if i > 0 {
+ fmt.Print(" ")
+ }
+ if i%2 == 0 {
+ fmt.Printf("%d.%s", (i/2)+1, move)
+ } else {
+ fmt.Printf(" %s", move)
+ }
+ }
+ fmt.Println()
+ }
+
+ // Display last move info
+ if game.LastMove != nil {
+ color := "White"
+ if game.LastMove.PlayerColor == "b" {
+ color = "Black"
+ }
+ fmt.Printf("Last move: %s by %s", game.LastMove.Move, color)
+ if game.LastMove.Depth > 0 {
+ fmt.Printf(" (depth %d, score %d)", game.LastMove.Depth, game.LastMove.Score)
+ }
+ fmt.Println()
+ }
+
+ return nil
+}
+
+func gameStateHandler(s Session, args []string) error {
+ gameID := s.GetCurrentGame()
+ if gameID == "" {
+ return fmt.Errorf("no current game, use 'new' or 'join '")
+ }
+
+ c := s.GetClient().(*api.Client)
+ resp, err := c.GetGame(gameID)
+ if err != nil {
+ return err
+ }
+
+ s.SetLastMoveCount(len(resp.Moves))
+
+ // Pretty print JSON
+ fmt.Printf("%sGame State:%s\n", display.Cyan, display.Reset)
+ display.PrettyPrintJSON(resp)
+
+ return nil
+}
+
+func deleteGameHandler(s Session, args []string) error {
+ gameID := s.GetCurrentGame()
+ if len(args) > 0 {
+ gameID = args[0]
+ }
+
+ if gameID == "" {
+ return fmt.Errorf("specify game ID or set current game")
+ }
+
+ c := s.GetClient().(*api.Client)
+ err := c.DeleteGame(gameID)
+ if err != nil {
+ return err
+ }
+
+ if gameID == s.GetCurrentGame() {
+ s.SetCurrentGame("")
+ s.SetLastMoveCount(0)
+ }
+
+ fmt.Printf("%sGame deleted: %s%s\n", display.Green, gameID, display.Reset)
+ return nil
+}
+
+func pollHandler(s Session, args []string) error {
+ gameID := s.GetCurrentGame()
+ if gameID == "" {
+ return fmt.Errorf("no current game, use 'new' or 'join '")
+ }
+
+ c := s.GetClient().(*api.Client)
+ moveCount := s.GetLastMoveCount()
+
+ fmt.Printf("%sLong-polling for updates (move count: %d)...%s\n",
+ display.Cyan, moveCount, display.Reset)
+ fmt.Printf("%sThis may take up to 25 seconds%s\n", display.Cyan, display.Reset)
+
+ resp, err := c.GetGameWithPoll(gameID, moveCount)
+ if err != nil {
+ return err
+ }
+
+ s.SetLastMoveCount(len(resp.Moves))
+ s.SetGameState(resp)
+
+ if len(resp.Moves) > moveCount {
+ fmt.Printf("%sGame updated! New moves detected%s\n", display.Green, display.Reset)
+ if resp.LastMove != nil {
+ fmt.Printf("Last move: %s\n", resp.LastMove.Move)
+ }
+ } else {
+ fmt.Printf("%sNo updates (timeout)%s\n", display.Yellow, display.Reset)
+ }
+
+ return nil
+}
\ No newline at end of file
diff --git a/internal/client/commands/registry.go b/internal/client/commands/registry.go
new file mode 100644
index 0000000..12884bc
--- /dev/null
+++ b/internal/client/commands/registry.go
@@ -0,0 +1,194 @@
+// FILE: lixenwraith/chess/internal/client/commands/registry.go
+package commands
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "chess/internal/client/api"
+ "chess/internal/client/display"
+)
+
+type Session interface {
+ GetAPIBaseURL() string
+ SetAPIBaseURL(string)
+ GetCurrentGame() string
+ SetCurrentGame(string)
+ GetCurrentUser() string
+ SetCurrentUser(string)
+ GetAuthToken() string
+ SetAuthToken(string)
+ GetUsername() string
+ SetUsername(string)
+ GetLastMoveCount() int
+ SetLastMoveCount(int)
+ GetClient() interface{}
+ IsVerbose() bool
+ SetGameState(interface{})
+ SetPlayerColor(string)
+ GetPlayerColor() string
+}
+
+// Command defines a client command with its handler
+type Command struct {
+ Name string
+ ShortName string
+ Description string
+ Usage string
+ Handler func(Session, []string) error
+}
+
+type Registry struct {
+ session Session
+ commands map[string]*Command
+}
+
+// Registry manages command registration and execution
+func NewRegistry(session Session) *Registry {
+ r := &Registry{
+ session: session,
+ commands: make(map[string]*Command),
+ }
+
+ // Register all commands
+ r.registerGameCommands()
+ r.registerAuthCommands()
+ r.registerDebugCommands()
+
+ // Help command
+ r.Register(&Command{
+ Name: "help",
+ ShortName: "?",
+ Description: "Show available commands",
+ Usage: "help [command]",
+ Handler: r.helpHandler,
+ })
+
+ // Exit command
+ r.Register(&Command{
+ Name: "exit",
+ ShortName: "x",
+ Description: "Exit the client",
+ Usage: "exit",
+ Handler: exitHandler,
+ })
+
+ return r
+}
+
+func (r *Registry) Register(cmd *Command) {
+ r.commands[cmd.Name] = cmd
+ if cmd.ShortName != "" {
+ r.commands[cmd.ShortName] = cmd
+ }
+}
+
+func (r *Registry) Execute(input string) {
+ parts := strings.Fields(input)
+ if len(parts) == 0 {
+ return
+ }
+
+ cmdName := parts[0]
+ args := parts[1:]
+
+ cmd, exists := r.commands[cmdName]
+ if !exists {
+ fmt.Printf("%sUnknown command: %s%s\n", display.Red, cmdName, display.Reset)
+ fmt.Printf("Type 'help' for available commands\n")
+ return
+ }
+
+ // Set verbose mode in client if session supports it
+ if cl, ok := r.session.GetClient().(*api.Client); ok {
+ cl.SetVerbose(r.session.IsVerbose())
+ }
+
+ if err := cmd.Handler(r.session, args); err != nil {
+ fmt.Printf("%sError: %s%s\n", display.Red, err.Error(), display.Reset)
+ }
+}
+
+func (r *Registry) helpHandler(s Session, args []string) error {
+ if len(args) > 0 {
+ // Show help for specific command
+ cmd, exists := r.commands[args[0]]
+ if !exists {
+ return fmt.Errorf("unknown command: %s", args[0])
+ }
+ fmt.Printf("\n%s%s%s - %s\n", display.Cyan, cmd.Name, display.Reset, cmd.Description)
+ if cmd.ShortName != "" {
+ fmt.Printf("Short form: %s%s%s\n", display.Cyan, cmd.ShortName, display.Reset)
+ }
+ fmt.Printf("Usage: %s\n", cmd.Usage)
+ return nil
+ }
+
+ // Show all commands
+ fmt.Printf("\n%sAvailable Commands:%s\n\n", display.Cyan, display.Reset)
+
+ // Group commands
+ type cmdInfo struct {
+ name string
+ shortName string
+ desc string
+ }
+
+ gameCommands := []cmdInfo{
+ {"new", "n", ""},
+ {"join", "j", ""},
+ {"move", "m", ""},
+ {"computer", "c", ""},
+ {"undo", "u", ""},
+ {"show", "h", ""},
+ {"state", "s", ""},
+ {"delete", "d", ""},
+ {"poll", "p", ""},
+ }
+
+ authCommands := []cmdInfo{
+ {"register", "r", ""},
+ {"login", "l", ""},
+ {"logout", "o", ""},
+ {"whoami", "i", ""},
+ {"user", "e", ""},
+ }
+
+ utilCommands := []cmdInfo{
+ {"health", ".", ""},
+ {"url", "/", ""},
+ {"raw", ":", ""},
+ {"clear", "-", ""},
+ {"help", "?", ""},
+ {"exit", "x", ""},
+ }
+
+ printCommandGroup := func(title string, cmds []cmdInfo) {
+ fmt.Printf("%s%s:%s\n", display.Yellow, title, display.Reset)
+ for _, info := range cmds {
+ if cmd, exists := r.commands[info.name]; exists {
+ shortPart := ""
+ if info.shortName != "" {
+ shortPart = fmt.Sprintf("[%s%s%s] ", display.Cyan, info.shortName, display.Reset)
+ }
+ fmt.Printf(" %s%-10s %s\n", shortPart, cmd.Name, cmd.Description)
+ }
+ }
+ }
+
+ printCommandGroup("Game Commands", gameCommands)
+ fmt.Println()
+ printCommandGroup("Auth Commands", authCommands)
+ fmt.Println()
+ printCommandGroup("Utility Commands", utilCommands)
+
+ fmt.Printf("\nType 'help ' for detailed usage\n")
+ fmt.Printf("Add '-v' to any command for verbose output\n")
+ return nil
+}
+func exitHandler(s Session, args []string) error {
+ fmt.Printf("%sGoodbye!%s\n", display.Cyan, display.Reset)
+ os.Exit(0)
+ return nil
+}
\ No newline at end of file
diff --git a/internal/client/display/board.go b/internal/client/display/board.go
new file mode 100644
index 0000000..7f9ca80
--- /dev/null
+++ b/internal/client/display/board.go
@@ -0,0 +1,54 @@
+// FILE: lixenwraith/chess/internal/client/display/board.go
+package display
+
+import (
+ "fmt"
+ "strings"
+)
+
+// RenderBoard renders an ASCII board with colored pieces
+func RenderBoard(asciiBoard string) {
+ lines := strings.Split(asciiBoard, "\n")
+
+ for i, line := range lines {
+ if strings.TrimSpace(line) == "" {
+ continue
+ }
+
+ isRankLine := (i == 0) || (i == 9)
+
+ // Process each character
+ for _, char := range line {
+ switch {
+ case char >= 'a' && char <= 'h' && isRankLine:
+ // File letters - Cyan
+ fmt.Printf("%s%c%s", Cyan, char, Reset)
+ case char >= 'A' && char <= 'Z':
+ // White pieces - Blue
+ fmt.Printf("%s%c%s", Blue, char, Reset)
+ case char >= 'a' && char <= 'z' && !isRankLine:
+ // Black pieces - Red
+ fmt.Printf("%s%c%s", Red, char, Reset)
+ case char == '.':
+ // Empty squares
+ fmt.Printf(".")
+ case char >= '1' && char <= '8':
+ // Rank numbers - Cyan
+ fmt.Printf("%s%c%s", Cyan, char, Reset)
+ case char == ' ':
+ fmt.Printf(" ")
+ default:
+ fmt.Printf("%c", char)
+ }
+ }
+ fmt.Println()
+ }
+}
+
+// ColorForTurn returns colored turn indicator
+func ColorForTurn(turn string) string {
+ if turn == "w" {
+ return Blue + "White" + Reset
+ }
+ return Red + "Black" + Reset
+}
\ No newline at end of file
diff --git a/internal/client/display/colors.go b/internal/client/display/colors.go
new file mode 100644
index 0000000..ee483e1
--- /dev/null
+++ b/internal/client/display/colors.go
@@ -0,0 +1,19 @@
+// FILE: lixenwraith/chess/internal/client/display/colors.go
+package display
+
+// Terminal color codes
+const (
+ Reset = "\033[0m"
+ Red = "\033[31m"
+ Green = "\033[32m"
+ Yellow = "\033[33m"
+ Blue = "\033[34m"
+ Magenta = "\033[35m"
+ Cyan = "\033[36m"
+ White = "\033[37m"
+)
+
+// Prompt returns a colored prompt string
+func Prompt(text string) string {
+ return Yellow + text + Yellow + " > " + Reset
+}
\ No newline at end of file
diff --git a/internal/client/display/format.go b/internal/client/display/format.go
new file mode 100644
index 0000000..c6d124c
--- /dev/null
+++ b/internal/client/display/format.go
@@ -0,0 +1,17 @@
+// FILE: lixenwraith/chess/internal/client/display/format.go
+package display
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// PrettyPrintJSON prints formatted JSON
+func PrettyPrintJSON(v interface{}) {
+ data, err := json.MarshalIndent(v, "", " ")
+ if err != nil {
+ fmt.Printf("%sError formatting JSON: %s%s\n", Red, err.Error(), Reset)
+ return
+ }
+ fmt.Println(string(data))
+}
\ No newline at end of file
diff --git a/internal/client/session/session.go b/internal/client/session/session.go
new file mode 100644
index 0000000..61784b0
--- /dev/null
+++ b/internal/client/session/session.go
@@ -0,0 +1,44 @@
+// FILE: lixenwraith/chess/internal/client/session/session.go
+package session
+
+import (
+ "chess/internal/client/api"
+)
+
+// Session maintains client state and configuration
+type Session struct {
+ APIBaseURL string
+ CurrentGame string
+ CurrentUser string
+ AuthToken string
+ Username string
+ LastMoveCount int
+ Client *api.Client
+ Verbose bool
+ // Game state for prompt
+ CurrentGameState *api.GameResponse
+ PlayerColor string // "w", "b", or ""
+}
+
+// Session interface implementation
+func (s *Session) GetAPIBaseURL() string { return s.APIBaseURL }
+func (s *Session) SetAPIBaseURL(url string) { s.APIBaseURL = url }
+func (s *Session) GetCurrentGame() string { return s.CurrentGame }
+func (s *Session) SetCurrentGame(id string) { s.CurrentGame = id }
+func (s *Session) GetCurrentUser() string { return s.CurrentUser }
+func (s *Session) SetCurrentUser(id string) { s.CurrentUser = id }
+func (s *Session) GetAuthToken() string { return s.AuthToken }
+func (s *Session) SetAuthToken(token string) { s.AuthToken = token }
+func (s *Session) GetUsername() string { return s.Username }
+func (s *Session) SetUsername(name string) { s.Username = name }
+func (s *Session) GetLastMoveCount() int { return s.LastMoveCount }
+func (s *Session) SetLastMoveCount(count int) { s.LastMoveCount = count }
+func (s *Session) GetClient() interface{} { return s.Client }
+func (s *Session) IsVerbose() bool { return s.Verbose }
+func (s *Session) SetGameState(game interface{}) {
+ if g, ok := game.(*api.GameResponse); ok {
+ s.CurrentGameState = g
+ }
+}
+func (s *Session) SetPlayerColor(color string) { s.PlayerColor = color }
+func (s *Session) GetPlayerColor() string { return s.PlayerColor }
\ No newline at end of file
diff --git a/internal/board/board.go b/internal/server/board/board.go
similarity index 92%
rename from internal/board/board.go
rename to internal/server/board/board.go
index b9b38f5..b09a423 100644
--- a/internal/board/board.go
+++ b/internal/server/board/board.go
@@ -1,11 +1,11 @@
-// FILE: internal/board/board.go
+// FILE: lixenwraith/chess/internal/server/board/board.go
package board
import (
"fmt"
"strings"
- "chess/internal/core"
+ "chess/internal/server/core"
)
const (
@@ -21,6 +21,7 @@ type Board struct {
fullmove int
}
+// FromFEN creates a Board from a FEN string with validation
func ParseFEN(fen string) (*Board, error) {
parts := strings.Fields(fen)
if len(parts) != 6 {
@@ -95,7 +96,7 @@ func (b *Board) ToASCII() string {
sb.WriteString(fmt.Sprintf("%c ", piece))
}
}
- sb.WriteString(fmt.Sprintf(" %d\n", 8-r))
+ sb.WriteString(fmt.Sprintf("%d\n", 8-r))
}
sb.WriteString(" a b c d e f g h")
diff --git a/internal/core/api.go b/internal/server/core/api.go
similarity index 96%
rename from internal/core/api.go
rename to internal/server/core/api.go
index eb3af41..bac9c97 100644
--- a/internal/core/api.go
+++ b/internal/server/core/api.go
@@ -1,4 +1,4 @@
-// FILE: internal/core/api.go
+// FILE: lixenwraith/chess/internal/server/core/api.go
package core
// Request types
diff --git a/internal/core/error.go b/internal/server/core/error.go
similarity index 87%
rename from internal/core/error.go
rename to internal/server/core/error.go
index bace4a5..1fa4ebb 100644
--- a/internal/core/error.go
+++ b/internal/server/core/error.go
@@ -1,4 +1,4 @@
-// FILE: internal/core/core.go
+// FILE: lixenwraith/chess/internal/server/core/error.go
package core
// Error codes
diff --git a/internal/core/player.go b/internal/server/core/player.go
similarity index 96%
rename from internal/core/player.go
rename to internal/server/core/player.go
index 1a20d7f..9c3aa22 100644
--- a/internal/core/player.go
+++ b/internal/server/core/player.go
@@ -1,4 +1,4 @@
-// FILE: internal/core/player.go
+// FILE: lixenwraith/chess/internal/server/core/player.go
package core
import (
diff --git a/internal/core/state.go b/internal/server/core/state.go
similarity index 91%
rename from internal/core/state.go
rename to internal/server/core/state.go
index cd2081f..b7d2bac 100644
--- a/internal/core/state.go
+++ b/internal/server/core/state.go
@@ -1,4 +1,4 @@
-// FILE: internal/core/core.go
+// FILE: lixenwraith/chess/internal/server/core/state.go
package core
type State int
diff --git a/internal/engine/engine.go b/internal/server/engine/engine.go
similarity index 98%
rename from internal/engine/engine.go
rename to internal/server/engine/engine.go
index ff1df39..a3099d9 100644
--- a/internal/engine/engine.go
+++ b/internal/server/engine/engine.go
@@ -1,4 +1,4 @@
-// FILE: internal/engine/engine.go
+// FILE: lixenwraith/chess/internal/server/engine/engine.go
package engine
import (
diff --git a/internal/game/game.go b/internal/server/game/game.go
similarity index 94%
rename from internal/game/game.go
rename to internal/server/game/game.go
index bbf6e2d..0cbc09e 100644
--- a/internal/game/game.go
+++ b/internal/server/game/game.go
@@ -1,11 +1,11 @@
-// FILE: internal/game/game.go
+// FILE: lixenwraith/chess/internal/server/game/game.go
package game
import (
"fmt"
- "chess/internal/board"
- "chess/internal/core"
+ "chess/internal/server/board"
+ "chess/internal/server/core"
)
type Snapshot struct {
@@ -66,10 +66,12 @@ func (g *Game) LastResult() *MoveResult {
return g.lastResult
}
+// CurrentSnapshot returns the latest game snapshot
func (g *Game) CurrentSnapshot() Snapshot {
return g.snapshots[len(g.snapshots)-1]
}
+// CurrentFEN returns the current position in FEN notation
func (g *Game) CurrentFEN() string {
return g.CurrentSnapshot().FEN
}
diff --git a/internal/http/auth.go b/internal/server/http/auth.go
similarity index 98%
rename from internal/http/auth.go
rename to internal/server/http/auth.go
index 9c7f09e..34c3a3f 100644
--- a/internal/http/auth.go
+++ b/internal/server/http/auth.go
@@ -1,4 +1,4 @@
-// FILE: internal/http/auth.go
+// FILE: lixenwraith/chess/internal/server/http/auth.go
package http
import (
@@ -8,7 +8,7 @@ import (
"time"
"unicode"
- "chess/internal/core"
+ "chess/internal/server/core"
"github.com/gofiber/fiber/v2"
)
diff --git a/internal/http/handler.go b/internal/server/http/handler.go
similarity index 98%
rename from internal/http/handler.go
rename to internal/server/http/handler.go
index f59445d..b39c5de 100644
--- a/internal/http/handler.go
+++ b/internal/server/http/handler.go
@@ -1,4 +1,4 @@
-// FILE: internal/http/handler.go
+// FILE: lixenwraith/chess/internal/server/http/handler.go
package http
import (
@@ -7,9 +7,9 @@ import (
"strings"
"time"
- "chess/internal/core"
- "chess/internal/processor"
- "chess/internal/service"
+ "chess/internal/server/core"
+ "chess/internal/server/processor"
+ "chess/internal/server/service"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
@@ -20,6 +20,7 @@ import (
const rateLimitRate = 10 // req/sec
+// HTTPHandler handles HTTP requests and routes them to the processor
type HTTPHandler struct {
proc *processor.Processor
svc *service.Service
diff --git a/internal/http/middleware.go b/internal/server/http/middleware.go
similarity index 94%
rename from internal/http/middleware.go
rename to internal/server/http/middleware.go
index 92f35c8..559d7a5 100644
--- a/internal/http/middleware.go
+++ b/internal/server/http/middleware.go
@@ -1,11 +1,10 @@
-// FILE: internal/http/middleware.go
+// FILE: lixenwraith/chess/internal/server/http/middleware.go
package http
import (
+ "chess/internal/server/core"
"strings"
- "chess/internal/core"
-
"github.com/gofiber/fiber/v2"
)
diff --git a/internal/http/validator.go b/internal/server/http/validator.go
similarity index 97%
rename from internal/http/validator.go
rename to internal/server/http/validator.go
index 659b248..6569402 100644
--- a/internal/http/validator.go
+++ b/internal/server/http/validator.go
@@ -1,13 +1,12 @@
-// FILE: internal/http/handler.go
+// FILE: lixenwraith/chess/internal/server/http/handler.go
package http
import (
+ "chess/internal/server/core"
"fmt"
"reflect"
"strings"
- "chess/internal/core"
-
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
diff --git a/internal/processor/command.go b/internal/server/processor/command.go
similarity index 94%
rename from internal/processor/command.go
rename to internal/server/processor/command.go
index 89e993e..1113da0 100644
--- a/internal/processor/command.go
+++ b/internal/server/processor/command.go
@@ -1,8 +1,8 @@
-// FILE: internal/processor/command.go
+// FILE: lixenwraith/chess/internal/server/processor/command.go
package processor
import (
- "chess/internal/core"
+ "chess/internal/server/core"
)
// CommandType defines the type of command being executed
diff --git a/internal/processor/processor.go b/internal/server/processor/processor.go
similarity index 98%
rename from internal/processor/processor.go
rename to internal/server/processor/processor.go
index 0ba7a10..2a1618c 100644
--- a/internal/processor/processor.go
+++ b/internal/server/processor/processor.go
@@ -1,4 +1,4 @@
-// FILE: internal/processor/processor.go
+// FILE: lixenwraith/chess/internal/server/processor/processor.go
package processor
@@ -11,11 +11,11 @@ import (
"time"
"unicode"
- "chess/internal/board"
- "chess/internal/core"
- "chess/internal/engine"
- "chess/internal/game"
- "chess/internal/service"
+ "chess/internal/server/board"
+ "chess/internal/server/core"
+ "chess/internal/server/engine"
+ "chess/internal/server/game"
+ "chess/internal/server/service"
)
const (
@@ -25,7 +25,7 @@ const (
// FEN validation regex
var fenPattern = regexp.MustCompile(`^[rnbqkpRNBQKP1-8/]+ [wb] [KQkq-]+ [a-h1-8-]+ \d+ \d+$`)
-// Processor orchestrates all game logic and engine interactions
+// Processor handles command execution and coordinates between service and engine layers
type Processor struct {
svc *service.Service
queue *EngineQueue
diff --git a/internal/processor/queue.go b/internal/server/processor/queue.go
similarity index 95%
rename from internal/processor/queue.go
rename to internal/server/processor/queue.go
index d542d48..4534a19 100644
--- a/internal/processor/queue.go
+++ b/internal/server/processor/queue.go
@@ -1,4 +1,4 @@
-// FILE: internal/processor/queue.go
+// FILE: lixenwraith/chess/internal/server/processor/queue.go
package processor
import (
@@ -7,11 +7,11 @@ import (
"sync"
"time"
- "chess/internal/core"
- "chess/internal/engine"
+ "chess/internal/server/core"
+ "chess/internal/server/engine"
)
-// EngineTask represents a computer move calculation request
+// EngineTask contains computer move calculation request and response channel
type EngineTask struct {
GameID string
FEN string
diff --git a/internal/service/game.go b/internal/server/service/game.go
similarity index 96%
rename from internal/service/game.go
rename to internal/server/service/game.go
index c6985f4..1302bdf 100644
--- a/internal/service/game.go
+++ b/internal/server/service/game.go
@@ -1,13 +1,13 @@
-// FILE: internal/service/game.go
+// FILE: lixenwraith/chess/internal/server/service/game.go
package service
import (
"fmt"
"time"
- "chess/internal/core"
- "chess/internal/game"
- "chess/internal/storage"
+ "chess/internal/server/core"
+ "chess/internal/server/game"
+ "chess/internal/server/storage"
"github.com/google/uuid"
)
diff --git a/internal/service/service.go b/internal/server/service/service.go
similarity index 92%
rename from internal/service/service.go
rename to internal/server/service/service.go
index 9e045c7..0f0c091 100644
--- a/internal/service/service.go
+++ b/internal/server/service/service.go
@@ -1,4 +1,4 @@
-// FILE: internal/service/service.go
+// FILE: lixenwraith/chess/internal/server/service/service.go
package service
import (
@@ -8,8 +8,8 @@ import (
"sync"
"time"
- "chess/internal/game"
- "chess/internal/storage"
+ "chess/internal/server/game"
+ "chess/internal/server/storage"
)
// Service is a pure state manager for chess games with optional persistence
diff --git a/internal/service/user.go b/internal/server/service/user.go
similarity index 97%
rename from internal/service/user.go
rename to internal/server/service/user.go
index f45ba15..ca258df 100644
--- a/internal/service/user.go
+++ b/internal/server/service/user.go
@@ -1,4 +1,4 @@
-// FILE: internal/service/user.go
+// FILE: lixenwraith/chess/internal/server/service/user.go
package service
import (
@@ -6,7 +6,7 @@ import (
"strings"
"time"
- "chess/internal/storage"
+ "chess/internal/server/storage"
"github.com/google/uuid"
"github.com/lixenwraith/auth"
diff --git a/internal/service/waiter.go b/internal/server/service/waiter.go
similarity index 96%
rename from internal/service/waiter.go
rename to internal/server/service/waiter.go
index cd533d7..0e40bcb 100644
--- a/internal/service/waiter.go
+++ b/internal/server/service/waiter.go
@@ -1,4 +1,4 @@
-// FILE: internal/service/waiter.go
+// FILE: lixenwraith/chess/internal/server/service/waiter.go
package service
import (
@@ -16,7 +16,7 @@ const (
WaitChannelBuffer = 1
)
-// WaitRegistry manages long-polling clients waiting for game state changes
+// WaitRegistry manages clients waiting for game state changes via long-polling
type WaitRegistry struct {
mu sync.RWMutex
waiters map[string][]*WaitRequest // gameID → waiting clients
diff --git a/internal/storage/game.go b/internal/server/storage/game.go
similarity index 98%
rename from internal/storage/game.go
rename to internal/server/storage/game.go
index 7a5ae5d..7cbd4ae 100644
--- a/internal/storage/game.go
+++ b/internal/server/storage/game.go
@@ -1,4 +1,4 @@
-// FILE: internal/storage/game.go
+// FILE: lixenwraith/chess/internal/server/storage/game.go
package storage
import (
diff --git a/internal/storage/schema.go b/internal/server/storage/schema.go
similarity index 98%
rename from internal/storage/schema.go
rename to internal/server/storage/schema.go
index ff0507a..7a7e235 100644
--- a/internal/storage/schema.go
+++ b/internal/server/storage/schema.go
@@ -1,4 +1,4 @@
-// FILE: internal/storage/schema.go
+// FILE: lixenwraith/chess/internal/server/storage/schema.go
package storage
import "time"
diff --git a/internal/storage/storage.go b/internal/server/storage/storage.go
similarity index 97%
rename from internal/storage/storage.go
rename to internal/server/storage/storage.go
index 05b236a..cfd7f78 100644
--- a/internal/storage/storage.go
+++ b/internal/server/storage/storage.go
@@ -1,4 +1,4 @@
-// FILE: internal/storage/storage.go
+// FILE: lixenwraith/chess/internal/server/storage/storage.go
package storage
import (
@@ -70,7 +70,7 @@ func NewStore(dataSourceName string, devMode bool) (*Store, error) {
return s, nil
}
-// IsHealthy returns the current health status
+// IsHealthy returns true if the storage is operational
func (s *Store) IsHealthy() bool {
return s.healthStatus.Load()
}
diff --git a/internal/storage/user.go b/internal/server/storage/user.go
similarity index 98%
rename from internal/storage/user.go
rename to internal/server/storage/user.go
index 47546e1..db0a3a3 100644
--- a/internal/storage/user.go
+++ b/internal/server/storage/user.go
@@ -1,4 +1,4 @@
-// FILE: internal/storage/user.go
+// FILE: lixenwraith/chess/internal/server/storage/user.go
package storage
import (
diff --git a/internal/webserver/server.go b/internal/server/webserver/server.go
similarity index 97%
rename from internal/webserver/server.go
rename to internal/server/webserver/server.go
index 877b6e3..5ec4830 100644
--- a/internal/webserver/server.go
+++ b/internal/server/webserver/server.go
@@ -1,3 +1,4 @@
+// FILE: lixenwraith/chess/internal/server/webserver/server.go
package webserver
import (
diff --git a/internal/webserver/web/app.js b/internal/server/webserver/web/app.js
similarity index 99%
rename from internal/webserver/web/app.js
rename to internal/server/webserver/web/app.js
index 7202e27..d4d527f 100644
--- a/internal/webserver/web/app.js
+++ b/internal/server/webserver/web/app.js
@@ -1,4 +1,4 @@
-// FILE: internal/webserver/web/app.js
+// FILE: lixenwraith/chess/internal/server/webserver/web/app.js
// Game state management
let gameState = {
gameId: null,
diff --git a/internal/webserver/web/index.html b/internal/server/webserver/web/index.html
similarity index 98%
rename from internal/webserver/web/index.html
rename to internal/server/webserver/web/index.html
index 33953a2..b1deb45 100644
--- a/internal/webserver/web/index.html
+++ b/internal/server/webserver/web/index.html
@@ -1,4 +1,4 @@
-
+
diff --git a/internal/webserver/web/style.css b/internal/server/webserver/web/style.css
similarity index 99%
rename from internal/webserver/web/style.css
rename to internal/server/webserver/web/style.css
index 731cde3..b217d29 100644
--- a/internal/webserver/web/style.css
+++ b/internal/server/webserver/web/style.css
@@ -1,4 +1,4 @@
-/* FILE: internal/webserver/web/style.css */
+/* FILE: lixenwraith/chess/internal/server/webserver/web/style.css */
* {
margin: 0;
padding: 0;
diff --git a/test/run-test-server.sh b/test/run-test-server.sh
index 3bbf5f5..7de5dcc 100755
--- a/test/run-test-server.sh
+++ b/test/run-test-server.sh
@@ -1,12 +1,12 @@
#!/usr/bin/env bash
-# FILE: test/run-test-server.sh
+# FILE: lixenwraith/chess/test/run-test-server.sh
set -e
# Configuration
-CHESSD_EXEC=${1:-"./chessd"}
+CHESS_SERVER_EXEC=${1:-"bin/chess-server"}
TEST_DB="test.db"
-PID_FILE="/tmp/chessd_test.pid"
+PID_FILE="/tmp/chess-server_test.pid"
API_PORT=${API_PORT:-8080}
# Colors for output
@@ -17,10 +17,10 @@ CYAN='\033[0;36m'
NC='\033[0m'
# Check executable
-if [ ! -x "$CHESSD_EXEC" ]; then
- echo -e "${RED}Error: chessd executable not found or not executable: $CHESSD_EXEC${NC}"
- echo "Provide the path to chessd binary as first argument or place it in the current directory."
- echo "Build the binary if not available: go build ./cmd/chessd"
+if [ ! -x "$CHESS_SERVER_EXEC" ]; then
+ echo -e "${RED}Error: chess-server executable not found or not executable: $CHESS_SERVER_EXEC${NC}"
+ echo "Provide the path to chess-server binary as first argument or place it in the current directory."
+ echo "Build the binary if not available: go build ./cmd/chess-server"
exit 1
fi
@@ -32,7 +32,7 @@ cleanup() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
- echo "Stopping chessd server (PID: $PID)"
+ echo "Stopping chess-server server (PID: $PID)"
kill "$PID" 2>/dev/null || true
sleep 0.5
kill -9 "$PID" 2>/dev/null || true
@@ -56,7 +56,7 @@ rm -f "$TEST_DB" "${TEST_DB}-wal" "${TEST_DB}-shm" "$PID_FILE"
# Initialize database
echo -e "${CYAN}Initializing test database...${NC}"
-"$CHESSD_EXEC" db init -path "$TEST_DB"
+"$CHESS_SERVER_EXEC" db init -path "$TEST_DB"
if [ $? -ne 0 ]; then
echo -e "${RED}Failed to initialize database${NC}"
exit 1
@@ -64,14 +64,14 @@ fi
# Add test users
echo -e "${CYAN}Adding test users...${NC}"
-"$CHESSD_EXEC" db user add -path "$TEST_DB" \
+"$CHESS_SERVER_EXEC" db user add -path "$TEST_DB" \
-username alice -email alice@test.com -password AlicePass123
if [ $? -ne 0 ]; then
echo -e "${RED}Failed to create user alice${NC}"
exit 1
fi
-"$CHESSD_EXEC" db user add -path "$TEST_DB" \
+"$CHESS_SERVER_EXEC" db user add -path "$TEST_DB" \
-username bob -email bob@test.com -password BobSecure456
if [ $? -ne 0 ]; then
echo -e "${RED}Failed to create user bob${NC}"
@@ -84,15 +84,15 @@ echo " • bob / BobSecure456"
# Start server
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
-echo -e "${GREEN}║ Chess API Test Server with User Management ║${NC}"
+echo -e "${GREEN}║ Chess API Test Server with User Management ║${NC}"
echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
echo ""
echo "Configuration:"
-echo " Executable: $CHESSD_EXEC"
+echo " Executable: $CHESS_SERVER_EXEC"
echo " Database: $TEST_DB"
echo " Port: $API_PORT"
echo " Mode: Development (WAL enabled, relaxed rate limits)"
-echo " Purpose: Backend for chessd tests"
+echo " Purpose: Backend for chess-server tests"
echo " PID File: $PID_FILE"
echo ""
echo -e "${YELLOW}Instructions:${NC}"
@@ -104,8 +104,8 @@ echo -e "${CYAN}─────────────────────
echo "Starting server..."
echo ""
-# Start chessd in foreground with dev mode and storage
-"$CHESSD_EXEC" \
+# Start chess-server in foreground with dev mode and storage
+"$CHESS_SERVER_EXEC" \
-dev \
-storage-path "$TEST_DB" \
-api-port "$API_PORT" \
diff --git a/test/test-api.sh b/test/test-api.sh
index cb50331..f144cee 100755
--- a/test/test-api.sh
+++ b/test/test-api.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-# FILE: test/test-api.sh
+# FILE: lixenwraith/chess/test/test-api.sh
# Chess API Robustness Test Suite
# Tests the refactored chess API with security hardening
@@ -134,7 +134,7 @@ echo "Server: $BASE_URL"
echo "API Version: v1"
echo -e "${MAGENTA} IMPORTANT: Server must be started with -dev flag for tests to pass!${NC}"
echo -e "${MAGENTA} Start the server first: test/run-test-server.sh${NC}"
-echo -e "${MAGENTA} Or directly after build: ./chessd -dev${NC}"
+echo -e "${MAGENTA} Or directly after build: bin/chess-server -dev${NC}"
echo ""
echo "Starting comprehensive tests..."
diff --git a/test/test-db.sh b/test/test-db.sh
index 79e86fa..a870ee6 100755
--- a/test/test-db.sh
+++ b/test/test-db.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-# FILE: test/test-db.sh
+# FILE: lixenwraith/chess/test/test-db.sh
# Database & Authentication API Integration Test Suite
# Tests user operations, authentication, and persistence via HTTP API
@@ -10,7 +10,7 @@
BASE_URL="http://localhost:8080"
API_URL="${BASE_URL}/api/v1"
TEST_DB="test.db"
-CHESSD_EXEC=${1:-"./chessd"}
+CHESS_SERVER_EXEC=${1:-"bin/chess-server"}
API_DELAY=${API_DELAY:-50}
# Colors
@@ -130,8 +130,8 @@ for cmd in jq sqlite3 curl; do
done
# Check executable exists
-if [ ! -x "$CHESSD_EXEC" ]; then
- echo -e "${RED}Error: chessd executable not found or not executable: $CHESSD_EXEC${NC}"
+if [ ! -x "$CHESS_SERVER_EXEC" ]; then
+ echo -e "${RED}Error: chess-server executable not found or not executable: $CHESS_SERVER_EXEC${NC}"
exit 1
fi
@@ -145,7 +145,7 @@ fi
# Start tests
print_header "Database & User Management Test Suite"
echo "Server: $BASE_URL"
-echo "Executable: $CHESSD_EXEC"
+echo "Executable: $CHESS_SERVER_EXEC"
echo "Test Database (server-managed): $TEST_DB"
echo ""
@@ -154,11 +154,11 @@ print_header "SECTION 1: CLI User Operations"
# ==============================================================================
test_case "1.1: database initialization"
-assert_command "$CHESSD_EXEC db init -path $TEST_DB" 0 "initialize database"
+assert_command "$CHESS_SERVER_EXEC db init -path $TEST_DB" 0 "initialize database"
# Create testuser1 first (not charlie)
test_case "1.2: Add First User via CLI"
-OUTPUT=$($CHESSD_EXEC db user add -path "$TEST_DB" -username "testuser1" \
+OUTPUT=$($CHESS_SERVER_EXEC db user add -path "$TEST_DB" -username "testuser1" \
-email "testuser1@test.com" -password "TestPass123" 2>&1)
if echo "$OUTPUT" | grep -qi "User created successfully"; then
echo -e "${GREEN} ✓ User created: testuser1${NC}"
@@ -169,7 +169,7 @@ else
fi
test_case "1.3: Add Second User"
-OUTPUT=$($CHESSD_EXEC db user add -path "$TEST_DB" -username "testuser2" \
+OUTPUT=$($CHESS_SERVER_EXEC db user add -path "$TEST_DB" -username "testuser2" \
-password "TestPass456" 2>&1)
if echo "$OUTPUT" | grep -qi "User created successfully"; then
echo -e "${GREEN} ✓ User created: testuser2${NC}"
@@ -181,7 +181,7 @@ fi
# Now test duplicate prevention with an existing user
test_case "1.4: Duplicate Username Prevention"
-assert_command "$CHESSD_EXEC db user add -path $TEST_DB -username testuser1 -password TestPass789" 1 \
+assert_command "$CHESS_SERVER_EXEC db user add -path $TEST_DB -username testuser1 -password TestPass789" 1 \
"Duplicate username rejected"
test_case "1.5: Login with Case-Insensitive Username (ALICE)"
@@ -197,11 +197,11 @@ else
fi
test_case "1.6: Update User Email"
-assert_command "$CHESSD_EXEC db user set-email -path $TEST_DB -username testuser2 -email testuser2_updated@test.com" 0 \
+assert_command "$CHESS_SERVER_EXEC db user set-email -path $TEST_DB -username testuser2 -email testuser2_updated@test.com" 0 \
"Email update"
test_case "1.7: Update User Password"
-assert_command "$CHESSD_EXEC db user set-password -path $TEST_DB -username testuser2 -password NewPass789" 0 \
+assert_command "$CHESS_SERVER_EXEC db user set-password -path $TEST_DB -username testuser2 -password NewPass789" 0 \
"Password update"
test_case "2.1: Health Check"
@@ -349,7 +349,7 @@ print_header "SECTION 5: Password Operations"
# Now TEST_PASS2_NEW is defined, this test should work
test_case "5.1: Update User Password via CLI for 'bob'"
-assert_command "$CHESSD_EXEC db user set-password -path $TEST_DB -username $TEST_USER2 -password $TEST_PASS2_NEW" 0 \
+assert_command "$CHESS_SERVER_EXEC db user set-password -path $TEST_DB -username $TEST_USER2 -password $TEST_PASS2_NEW" 0 \
"CLI password update for '$TEST_USER2'"
test_case "5.2: Login with NEW Password for 'bob'"
@@ -372,11 +372,11 @@ STATUS=$(api_request POST "$API_URL/auth/login" \
assert_status 401 "$STATUS" "Old password correctly rejected for '$TEST_USER2'"
test_case "5.4: Add new user '$TEST_USER_CLI' via CLI for hash test"
-assert_command "$CHESSD_EXEC db user add -path $TEST_DB -username $TEST_USER_CLI -password $TEST_PASS_CLI" 0 \
+assert_command "$CHESS_SERVER_EXEC db user add -path $TEST_DB -username $TEST_USER_CLI -password $TEST_PASS_CLI" 0 \
"Add user '$TEST_USER_CLI' for hash test"
test_case "5.5: CLI rejects unsupported hash format"
-assert_command "$CHESSD_EXEC db user set-hash -path $TEST_DB -username $TEST_USER_CLI -hash '$UNSUPPORTED_HASH'" 1 \
+assert_command "$CHESS_SERVER_EXEC db user set-hash -path $TEST_DB -username $TEST_USER_CLI -hash '$UNSUPPORTED_HASH'" 1 \
"Unsupported bcrypt hash rejected by CLI"
# ==============================================================================
@@ -419,11 +419,11 @@ fi
test_case "6.3: Delete User"
# First, get a user to delete
-OUTPUT=$($CHESSD_EXEC db user add -path "$TEST_DB" -username "deleteme" \
+OUTPUT=$($CHESS_SERVER_EXEC db user add -path "$TEST_DB" -username "deleteme" \
-password "TempPass123" 2>&1)
TEMP_ID=$(echo "$OUTPUT" | grep "ID:" | awk '{print $2}')
-assert_command "$CHESSD_EXEC db user delete -path $TEST_DB -username deleteme" 0 \
+assert_command "$CHESS_SERVER_EXEC db user delete -path $TEST_DB -username deleteme" 0 \
"User deletion by username"
# Verify deletion
diff --git a/test/test-longpoll.sh b/test/test-longpoll.sh
index 1b8e8a8..f2f81e0 100755
--- a/test/test-longpoll.sh
+++ b/test/test-longpoll.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# test-longpoll.sh - Test long-polling functionality
+# FILE: lixenwraith/chess/test/test-longpoll.sh
set -e