v0.7.0 cli client with readline added, directory structure updated
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,3 +4,5 @@ log
|
|||||||
bin
|
bin
|
||||||
db
|
db
|
||||||
build.sh
|
build.sh
|
||||||
|
.chess_history
|
||||||
|
|
||||||
|
|||||||
159
Makefile
Normal file
159
Makefile
Normal file
@ -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
|
||||||
58
README.md
58
README.md
@ -3,7 +3,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<h1>Go Chess API</h1>
|
<h1>Go Chess API</h1>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://golang.org"><img src="https://img.shields.io/badge/Go-1.24-00ADD8?style=flat&logo=go" alt="Go"></a>
|
<a href="https://golang.org"><img src="https://img.shields.io/badge/Go-1.25-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||||
<a href="https://opensource.org/licenses/BSD-3-Clause"><img src="https://img.shields.io/badge/License-BSD_3--Clause-blue.svg" alt="License"></a>
|
<a href="https://opensource.org/licenses/BSD-3-Clause"><img src="https://img.shields.io/badge/License-BSD_3--Clause-blue.svg" alt="License"></a>
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
@ -30,7 +30,7 @@ Go backend server providing a RESTful API for chess gameplay with user authentic
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Go 1.24+
|
- Go 1.25+
|
||||||
- Stockfish chess engine (`stockfish` in PATH)
|
- Stockfish chess engine (`stockfish` in PATH)
|
||||||
- SQLite3 (for persistence features)
|
- SQLite3 (for persistence features)
|
||||||
|
|
||||||
@ -44,24 +44,45 @@ pkg install stockfish
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## 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
|
```bash
|
||||||
#git clone https://git.lixen.com/lixen/chess # Mirror
|
#git clone https://git.lixen.com/lixen/chess # Mirror
|
||||||
git clone https://github.com/lixenwraith/chess
|
git clone https://github.com/lixenwraith/chess
|
||||||
cd chess
|
cd chess
|
||||||
go build ./cmd/chessd
|
go build ./cmd/chess-server
|
||||||
|
|
||||||
# Standard mode with persistence and auth
|
# Standard mode with persistence and auth
|
||||||
./chessd -storage-path chess.db
|
./chess-server -storage-path chess.db
|
||||||
|
|
||||||
# Development mode with all features
|
# 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
|
# Initialize database with user support
|
||||||
./chessd db init -path chess.db
|
./chess-server db init -path chess.db
|
||||||
|
|
||||||
# Add users via CLI
|
# Add users via CLI
|
||||||
./chessd db user add -path chess.db -username alice -password AlicePass123
|
./chess-server db user add -path chess.db -username alice -password AlicePass123
|
||||||
./chessd db user list -path chess.db
|
./chess-server db user list -path chess.db
|
||||||
```
|
```
|
||||||
|
|
||||||
Server listens on `http://localhost:8080`. See [API Reference](./doc/api.md) for endpoints including authentication.
|
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
|
### Creating Users
|
||||||
```bash
|
```bash
|
||||||
# Add user with password
|
# 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
|
# 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
|
# 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
|
### Managing Users
|
||||||
```bash
|
```bash
|
||||||
# List all users
|
# List all users
|
||||||
./chessd db user list -path chess.db
|
./chess-server db user list -path chess.db
|
||||||
|
|
||||||
# Update password
|
# 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
|
# 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
|
# Delete user
|
||||||
./chessd db user delete -path chess.db -username alice
|
./chess-server db user delete -path chess.db -username alice
|
||||||
```
|
```
|
||||||
|
|
||||||
## Web UI
|
## Web UI
|
||||||
@ -104,13 +125,13 @@ The chess server includes an embedded web UI for playing games through a browser
|
|||||||
### Enabling Web UI
|
### Enabling Web UI
|
||||||
```bash
|
```bash
|
||||||
# Start with web UI on default port 9090
|
# Start with web UI on default port 9090
|
||||||
./chessd -serve
|
./chess-server -serve
|
||||||
|
|
||||||
# Custom web UI port
|
# 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
|
# 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
|
### 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
|
- [API Reference](./doc/api.md) - Endpoint specifications including auth
|
||||||
- [Architecture](./doc/architecture.md) - System design with auth layer
|
- [Architecture](./doc/architecture.md) - System design with auth layer
|
||||||
- [Development](./doc/development.md) - Build, test, and user management
|
- [Development](./doc/development.md) - Build, test, and user management
|
||||||
|
- [Client Guide](./doc/client.md) - Interactive debugging client
|
||||||
- [Stockfish Integration](./doc/stockfish.md) - Engine communication
|
- [Stockfish Integration](./doc/stockfish.md) - Engine communication
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
139
cmd/chess-client/main.go
Normal file
139
cmd/chess-client/main.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: cmd/chessd/cli/cli.go
|
// FILE: lixenwraith/chess/cmd/chess-server/cli/cli.go
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -10,7 +10,7 @@ import (
|
|||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"chess/internal/storage"
|
"chess/internal/server/storage"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/lixenwraith/auth"
|
"github.com/lixenwraith/auth"
|
||||||
@ -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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -12,12 +14,12 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"chess/cmd/chessd/cli"
|
"chess/cmd/chess-server/cli"
|
||||||
"chess/internal/http"
|
"chess/internal/server/http"
|
||||||
"chess/internal/processor"
|
"chess/internal/server/processor"
|
||||||
"chess/internal/service"
|
"chess/internal/server/service"
|
||||||
"chess/internal/storage"
|
"chess/internal/server/storage"
|
||||||
"chess/internal/webserver"
|
"chess/internal/server/webserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: cmd/chessd/pid.go
|
// FILE: lixenwraith/chess/cmd/chess-server/pid.go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -10,8 +10,8 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// managePIDFile creates and manages a PID file with optional locking.
|
// managePIDFile creates and manages a PID file with optional locking
|
||||||
// Returns a cleanup function that must be called on exit.
|
// Returns a cleanup function that must be called on exit
|
||||||
func managePIDFile(path string, lock bool) (func(), error) {
|
func managePIDFile(path string, lock bool) (func(), error) {
|
||||||
// Open/create PID file with exclusive create first attempt
|
// Open/create PID file with exclusive create first attempt
|
||||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
||||||
@ -29,6 +29,7 @@ SQLite persistence with async writes for games, synchronous writes for authentic
|
|||||||
- **Board** (`internal/board`): FEN parsing and ASCII generation
|
- **Board** (`internal/board`): FEN parsing and ASCII generation
|
||||||
- **Core** (`internal/core`): Shared types, API models, error constants
|
- **Core** (`internal/core`): Shared types, API models, error constants
|
||||||
- **CLI** (`cmd/chessd/cli`): Database and user management commands
|
- **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
|
## Request Flow
|
||||||
|
|
||||||
|
|||||||
291
doc/client.md
Normal file
291
doc/client.md
Normal file
@ -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 <gameId> # 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/<id>
|
||||||
|
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).
|
||||||
@ -13,7 +13,8 @@
|
|||||||
#git clone https://git.lixen.com/lixen/chess # Mirror
|
#git clone https://git.lixen.com/lixen/chess # Mirror
|
||||||
git clone https://github.com/lixenwraith/chess
|
git clone https://github.com/lixenwraith/chess
|
||||||
cd chess
|
cd chess
|
||||||
go build ./cmd/chessd
|
go build ./cmd/chess-server
|
||||||
|
go build ./cmd/chess-client
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
@ -118,30 +119,39 @@ go build ./cmd/chessd
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
```
|
```
|
||||||
chess/
|
chess/
|
||||||
├── cmd/chessd/
|
├── cmd/
|
||||||
│ ├── main.go # Entry point with auth initialization
|
│ ├── chess-server/ # Server app
|
||||||
│ ├── pid.go # PID file management
|
│ │ ├── main.go # Server entry point
|
||||||
│ └── cli/ # Database and user CLI
|
│ │ ├── pid.go # PID file management
|
||||||
|
│ │ └── cli/ # Database and user CLI
|
||||||
|
│ └── chess-client/ # Client app
|
||||||
|
│ └── main.go # Interactive debugging client
|
||||||
├── internal/
|
├── internal/
|
||||||
│ ├── board/ # FEN/ASCII operations
|
│ ├── client/ # Client components
|
||||||
│ ├── core/ # Shared types and API models
|
│ │ ├── api/ # HTTP client for server API
|
||||||
│ ├── engine/ # Stockfish UCI wrapper
|
│ │ ├── commands/ # Command registry and handlers
|
||||||
│ ├── game/ # Game state with player associations
|
│ │ ├── display/ # Terminal output formatting
|
||||||
│ ├── http/ # Fiber handlers and auth endpoints
|
│ │ └── session/ # Session state management
|
||||||
│ │ ├── handler.go # Game endpoints
|
│ └── server/ # Server components
|
||||||
│ │ ├── auth.go # Authentication endpoints
|
│ ├── board/ # FEN/ASCII operations
|
||||||
│ │ └── middleware.go # JWT validation
|
│ ├── core/ # Shared types and API models
|
||||||
│ ├── processor/ # Command processing with user context
|
│ ├── engine/ # Stockfish UCI wrapper
|
||||||
│ ├── service/ # State and user management
|
│ ├── game/ # Game state with player associations
|
||||||
│ │ ├── service.go # Core service
|
│ ├── http/ # Fiber handlers and auth endpoints
|
||||||
│ │ ├── game.go # Game operations
|
│ │ ├── handler.go # Game endpoints
|
||||||
│ │ └── user.go # User and auth operations
|
│ │ ├── auth.go # Authentication endpoints
|
||||||
│ └── storage/ # SQLite persistence
|
│ │ └── middleware.go # JWT validation
|
||||||
│ ├── storage.go # Async writer for games
|
│ ├── processor/ # Command processing with user context
|
||||||
│ ├── game.go # Game persistence
|
│ ├── service/ # State and user management
|
||||||
│ ├── user.go # User persistence (synchronous)
|
│ │ ├── service.go # Core service
|
||||||
│ └── schema.go # Database schema
|
│ │ ├── game.go # Game operations
|
||||||
└── test/ # Test scripts
|
│ │ └── 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
|
## Testing
|
||||||
|
|||||||
11
go.mod
11
go.mod
@ -1,14 +1,15 @@
|
|||||||
module chess
|
module chess
|
||||||
|
|
||||||
go 1.25.3
|
go 1.25.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/chzyer/readline v1.5.1
|
||||||
github.com/go-playground/validator/v10 v10.28.0
|
github.com/go-playground/validator/v10 v10.28.0
|
||||||
github.com/gofiber/fiber/v2 v2.52.9
|
github.com/gofiber/fiber/v2 v2.52.9
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226
|
github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
golang.org/x/term v0.36.0
|
golang.org/x/term v0.37.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -28,7 +29,7 @@ require (
|
|||||||
github.com/tinylib/msgp v1.5.0 // indirect
|
github.com/tinylib/msgp v1.5.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.68.0 // indirect
|
github.com/valyala/fasthttp v1.68.0 // indirect
|
||||||
golang.org/x/crypto v0.43.0 // indirect
|
golang.org/x/crypto v0.44.0 // indirect
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.30.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
23
go.sum
23
go.sum
@ -1,5 +1,11 @@
|
|||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
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 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
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=
|
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/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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
241
internal/client/api/client.go
Normal file
241
internal/client/api/client.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
97
internal/client/api/types.go
Normal file
97
internal/client/api/types.go
Normal file
@ -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"`
|
||||||
|
}
|
||||||
181
internal/client/commands/auth.go
Normal file
181
internal/client/commands/auth.go
Normal file
@ -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 <userId>",
|
||||||
|
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>")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
108
internal/client/commands/debug.go
Normal file
108
internal/client/commands/debug.go
Normal file
@ -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 <method> <path> [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 <method> <path> [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()
|
||||||
|
}
|
||||||
511
internal/client/commands/game.go
Normal file
511
internal/client/commands/game.go
Normal file
@ -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 <gameId>",
|
||||||
|
Handler: joinGameHandler,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Register(&Command{
|
||||||
|
Name: "move",
|
||||||
|
ShortName: "m",
|
||||||
|
Description: "Make a move",
|
||||||
|
Usage: "move <uci-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>")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <uci-move>")
|
||||||
|
}
|
||||||
|
|
||||||
|
gameID := s.GetCurrentGame()
|
||||||
|
if gameID == "" {
|
||||||
|
return fmt.Errorf("no current game, use 'new' or 'join <gameId>'")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <gameId>'")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <gameId>'")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <gameId>'")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <gameId>'")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <gameId>'")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
194
internal/client/commands/registry.go
Normal file
194
internal/client/commands/registry.go
Normal file
@ -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 <command>' 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
|
||||||
|
}
|
||||||
54
internal/client/display/board.go
Normal file
54
internal/client/display/board.go
Normal file
@ -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
|
||||||
|
}
|
||||||
19
internal/client/display/colors.go
Normal file
19
internal/client/display/colors.go
Normal file
@ -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
|
||||||
|
}
|
||||||
17
internal/client/display/format.go
Normal file
17
internal/client/display/format.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
44
internal/client/session/session.go
Normal file
44
internal/client/session/session.go
Normal file
@ -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 }
|
||||||
@ -1,11 +1,11 @@
|
|||||||
// FILE: internal/board/board.go
|
// FILE: lixenwraith/chess/internal/server/board/board.go
|
||||||
package board
|
package board
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"chess/internal/core"
|
"chess/internal/server/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -21,6 +21,7 @@ type Board struct {
|
|||||||
fullmove int
|
fullmove int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromFEN creates a Board from a FEN string with validation
|
||||||
func ParseFEN(fen string) (*Board, error) {
|
func ParseFEN(fen string) (*Board, error) {
|
||||||
parts := strings.Fields(fen)
|
parts := strings.Fields(fen)
|
||||||
if len(parts) != 6 {
|
if len(parts) != 6 {
|
||||||
@ -95,7 +96,7 @@ func (b *Board) ToASCII() string {
|
|||||||
sb.WriteString(fmt.Sprintf("%c ", piece))
|
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")
|
sb.WriteString(" a b c d e f g h")
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/core/api.go
|
// FILE: lixenwraith/chess/internal/server/core/api.go
|
||||||
package core
|
package core
|
||||||
|
|
||||||
// Request types
|
// Request types
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/core/core.go
|
// FILE: lixenwraith/chess/internal/server/core/error.go
|
||||||
package core
|
package core
|
||||||
|
|
||||||
// Error codes
|
// Error codes
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/core/player.go
|
// FILE: lixenwraith/chess/internal/server/core/player.go
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/core/core.go
|
// FILE: lixenwraith/chess/internal/server/core/state.go
|
||||||
package core
|
package core
|
||||||
|
|
||||||
type State int
|
type State int
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/engine/engine.go
|
// FILE: lixenwraith/chess/internal/server/engine/engine.go
|
||||||
package engine
|
package engine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -1,11 +1,11 @@
|
|||||||
// FILE: internal/game/game.go
|
// FILE: lixenwraith/chess/internal/server/game/game.go
|
||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"chess/internal/board"
|
"chess/internal/server/board"
|
||||||
"chess/internal/core"
|
"chess/internal/server/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Snapshot struct {
|
type Snapshot struct {
|
||||||
@ -66,10 +66,12 @@ func (g *Game) LastResult() *MoveResult {
|
|||||||
return g.lastResult
|
return g.lastResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CurrentSnapshot returns the latest game snapshot
|
||||||
func (g *Game) CurrentSnapshot() Snapshot {
|
func (g *Game) CurrentSnapshot() Snapshot {
|
||||||
return g.snapshots[len(g.snapshots)-1]
|
return g.snapshots[len(g.snapshots)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CurrentFEN returns the current position in FEN notation
|
||||||
func (g *Game) CurrentFEN() string {
|
func (g *Game) CurrentFEN() string {
|
||||||
return g.CurrentSnapshot().FEN
|
return g.CurrentSnapshot().FEN
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/http/auth.go
|
// FILE: lixenwraith/chess/internal/server/http/auth.go
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"chess/internal/core"
|
"chess/internal/server/core"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/http/handler.go
|
// FILE: lixenwraith/chess/internal/server/http/handler.go
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -7,9 +7,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"chess/internal/core"
|
"chess/internal/server/core"
|
||||||
"chess/internal/processor"
|
"chess/internal/server/processor"
|
||||||
"chess/internal/service"
|
"chess/internal/server/service"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
@ -20,6 +20,7 @@ import (
|
|||||||
|
|
||||||
const rateLimitRate = 10 // req/sec
|
const rateLimitRate = 10 // req/sec
|
||||||
|
|
||||||
|
// HTTPHandler handles HTTP requests and routes them to the processor
|
||||||
type HTTPHandler struct {
|
type HTTPHandler struct {
|
||||||
proc *processor.Processor
|
proc *processor.Processor
|
||||||
svc *service.Service
|
svc *service.Service
|
||||||
@ -1,11 +1,10 @@
|
|||||||
// FILE: internal/http/middleware.go
|
// FILE: lixenwraith/chess/internal/server/http/middleware.go
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"chess/internal/server/core"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"chess/internal/core"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1,13 +1,12 @@
|
|||||||
// FILE: internal/http/handler.go
|
// FILE: lixenwraith/chess/internal/server/http/handler.go
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"chess/internal/server/core"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"chess/internal/core"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -1,8 +1,8 @@
|
|||||||
// FILE: internal/processor/command.go
|
// FILE: lixenwraith/chess/internal/server/processor/command.go
|
||||||
package processor
|
package processor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"chess/internal/core"
|
"chess/internal/server/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommandType defines the type of command being executed
|
// CommandType defines the type of command being executed
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/processor/processor.go
|
// FILE: lixenwraith/chess/internal/server/processor/processor.go
|
||||||
|
|
||||||
package processor
|
package processor
|
||||||
|
|
||||||
@ -11,11 +11,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"chess/internal/board"
|
"chess/internal/server/board"
|
||||||
"chess/internal/core"
|
"chess/internal/server/core"
|
||||||
"chess/internal/engine"
|
"chess/internal/server/engine"
|
||||||
"chess/internal/game"
|
"chess/internal/server/game"
|
||||||
"chess/internal/service"
|
"chess/internal/server/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -25,7 +25,7 @@ const (
|
|||||||
// FEN validation regex
|
// FEN validation regex
|
||||||
var fenPattern = regexp.MustCompile(`^[rnbqkpRNBQKP1-8/]+ [wb] [KQkq-]+ [a-h1-8-]+ \d+ \d+$`)
|
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 {
|
type Processor struct {
|
||||||
svc *service.Service
|
svc *service.Service
|
||||||
queue *EngineQueue
|
queue *EngineQueue
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/processor/queue.go
|
// FILE: lixenwraith/chess/internal/server/processor/queue.go
|
||||||
package processor
|
package processor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -7,11 +7,11 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"chess/internal/core"
|
"chess/internal/server/core"
|
||||||
"chess/internal/engine"
|
"chess/internal/server/engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EngineTask represents a computer move calculation request
|
// EngineTask contains computer move calculation request and response channel
|
||||||
type EngineTask struct {
|
type EngineTask struct {
|
||||||
GameID string
|
GameID string
|
||||||
FEN string
|
FEN string
|
||||||
@ -1,13 +1,13 @@
|
|||||||
// FILE: internal/service/game.go
|
// FILE: lixenwraith/chess/internal/server/service/game.go
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"chess/internal/core"
|
"chess/internal/server/core"
|
||||||
"chess/internal/game"
|
"chess/internal/server/game"
|
||||||
"chess/internal/storage"
|
"chess/internal/server/storage"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/service/service.go
|
// FILE: lixenwraith/chess/internal/server/service/service.go
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -8,8 +8,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"chess/internal/game"
|
"chess/internal/server/game"
|
||||||
"chess/internal/storage"
|
"chess/internal/server/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service is a pure state manager for chess games with optional persistence
|
// Service is a pure state manager for chess games with optional persistence
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/service/user.go
|
// FILE: lixenwraith/chess/internal/server/service/user.go
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"chess/internal/storage"
|
"chess/internal/server/storage"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/lixenwraith/auth"
|
"github.com/lixenwraith/auth"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/service/waiter.go
|
// FILE: lixenwraith/chess/internal/server/service/waiter.go
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -16,7 +16,7 @@ const (
|
|||||||
WaitChannelBuffer = 1
|
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 {
|
type WaitRegistry struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
waiters map[string][]*WaitRequest // gameID → waiting clients
|
waiters map[string][]*WaitRequest // gameID → waiting clients
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/storage/game.go
|
// FILE: lixenwraith/chess/internal/server/storage/game.go
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/storage/schema.go
|
// FILE: lixenwraith/chess/internal/server/storage/schema.go
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/storage/storage.go
|
// FILE: lixenwraith/chess/internal/server/storage/storage.go
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -70,7 +70,7 @@ func NewStore(dataSourceName string, devMode bool) (*Store, error) {
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsHealthy returns the current health status
|
// IsHealthy returns true if the storage is operational
|
||||||
func (s *Store) IsHealthy() bool {
|
func (s *Store) IsHealthy() bool {
|
||||||
return s.healthStatus.Load()
|
return s.healthStatus.Load()
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/storage/user.go
|
// FILE: lixenwraith/chess/internal/server/storage/user.go
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// FILE: lixenwraith/chess/internal/server/webserver/server.go
|
||||||
package webserver
|
package webserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// FILE: internal/webserver/web/app.js
|
// FILE: lixenwraith/chess/internal/server/webserver/web/app.js
|
||||||
// Game state management
|
// Game state management
|
||||||
let gameState = {
|
let gameState = {
|
||||||
gameId: null,
|
gameId: null,
|
||||||
@ -1,4 +1,4 @@
|
|||||||
<!-- FILE: internal/webserver/web/index.html -->
|
<!-- FILE: lixenwraith/chess/internal/server/webserver/web/index.html -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
/* FILE: internal/webserver/web/style.css */
|
/* FILE: lixenwraith/chess/internal/server/webserver/web/style.css */
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -1,12 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# FILE: test/run-test-server.sh
|
# FILE: lixenwraith/chess/test/run-test-server.sh
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
CHESSD_EXEC=${1:-"./chessd"}
|
CHESS_SERVER_EXEC=${1:-"bin/chess-server"}
|
||||||
TEST_DB="test.db"
|
TEST_DB="test.db"
|
||||||
PID_FILE="/tmp/chessd_test.pid"
|
PID_FILE="/tmp/chess-server_test.pid"
|
||||||
API_PORT=${API_PORT:-8080}
|
API_PORT=${API_PORT:-8080}
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
@ -17,10 +17,10 @@ CYAN='\033[0;36m'
|
|||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
# Check executable
|
# Check executable
|
||||||
if [ ! -x "$CHESSD_EXEC" ]; then
|
if [ ! -x "$CHESS_SERVER_EXEC" ]; then
|
||||||
echo -e "${RED}Error: chessd executable not found or not executable: $CHESSD_EXEC${NC}"
|
echo -e "${RED}Error: chess-server executable not found or not executable: $CHESS_SERVER_EXEC${NC}"
|
||||||
echo "Provide the path to chessd binary as first argument or place it in the current directory."
|
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/chessd"
|
echo "Build the binary if not available: go build ./cmd/chess-server"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ cleanup() {
|
|||||||
if [ -f "$PID_FILE" ]; then
|
if [ -f "$PID_FILE" ]; then
|
||||||
PID=$(cat "$PID_FILE")
|
PID=$(cat "$PID_FILE")
|
||||||
if kill -0 "$PID" 2>/dev/null; then
|
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
|
kill "$PID" 2>/dev/null || true
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
kill -9 "$PID" 2>/dev/null || true
|
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
|
# Initialize database
|
||||||
echo -e "${CYAN}Initializing test database...${NC}"
|
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
|
if [ $? -ne 0 ]; then
|
||||||
echo -e "${RED}Failed to initialize database${NC}"
|
echo -e "${RED}Failed to initialize database${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
@ -64,14 +64,14 @@ fi
|
|||||||
|
|
||||||
# Add test users
|
# Add test users
|
||||||
echo -e "${CYAN}Adding test users...${NC}"
|
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
|
-username alice -email alice@test.com -password AlicePass123
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo -e "${RED}Failed to create user alice${NC}"
|
echo -e "${RED}Failed to create user alice${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
-username bob -email bob@test.com -password BobSecure456
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo -e "${RED}Failed to create user bob${NC}"
|
echo -e "${RED}Failed to create user bob${NC}"
|
||||||
@ -84,15 +84,15 @@ echo " • bob / BobSecure456"
|
|||||||
|
|
||||||
# Start server
|
# Start server
|
||||||
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
|
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 -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Configuration:"
|
echo "Configuration:"
|
||||||
echo " Executable: $CHESSD_EXEC"
|
echo " Executable: $CHESS_SERVER_EXEC"
|
||||||
echo " Database: $TEST_DB"
|
echo " Database: $TEST_DB"
|
||||||
echo " Port: $API_PORT"
|
echo " Port: $API_PORT"
|
||||||
echo " Mode: Development (WAL enabled, relaxed rate limits)"
|
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 " PID File: $PID_FILE"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Instructions:${NC}"
|
echo -e "${YELLOW}Instructions:${NC}"
|
||||||
@ -104,8 +104,8 @@ echo -e "${CYAN}─────────────────────
|
|||||||
echo "Starting server..."
|
echo "Starting server..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Start chessd in foreground with dev mode and storage
|
# Start chess-server in foreground with dev mode and storage
|
||||||
"$CHESSD_EXEC" \
|
"$CHESS_SERVER_EXEC" \
|
||||||
-dev \
|
-dev \
|
||||||
-storage-path "$TEST_DB" \
|
-storage-path "$TEST_DB" \
|
||||||
-api-port "$API_PORT" \
|
-api-port "$API_PORT" \
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# FILE: test/test-api.sh
|
# FILE: lixenwraith/chess/test/test-api.sh
|
||||||
|
|
||||||
# Chess API Robustness Test Suite
|
# Chess API Robustness Test Suite
|
||||||
# Tests the refactored chess API with security hardening
|
# Tests the refactored chess API with security hardening
|
||||||
@ -134,7 +134,7 @@ echo "Server: $BASE_URL"
|
|||||||
echo "API Version: v1"
|
echo "API Version: v1"
|
||||||
echo -e "${MAGENTA} IMPORTANT: Server must be started with -dev flag for tests to pass!${NC}"
|
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} 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 ""
|
||||||
echo "Starting comprehensive tests..."
|
echo "Starting comprehensive tests..."
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# FILE: test/test-db.sh
|
# FILE: lixenwraith/chess/test/test-db.sh
|
||||||
|
|
||||||
# Database & Authentication API Integration Test Suite
|
# Database & Authentication API Integration Test Suite
|
||||||
# Tests user operations, authentication, and persistence via HTTP API
|
# Tests user operations, authentication, and persistence via HTTP API
|
||||||
@ -10,7 +10,7 @@
|
|||||||
BASE_URL="http://localhost:8080"
|
BASE_URL="http://localhost:8080"
|
||||||
API_URL="${BASE_URL}/api/v1"
|
API_URL="${BASE_URL}/api/v1"
|
||||||
TEST_DB="test.db"
|
TEST_DB="test.db"
|
||||||
CHESSD_EXEC=${1:-"./chessd"}
|
CHESS_SERVER_EXEC=${1:-"bin/chess-server"}
|
||||||
API_DELAY=${API_DELAY:-50}
|
API_DELAY=${API_DELAY:-50}
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
@ -130,8 +130,8 @@ for cmd in jq sqlite3 curl; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Check executable exists
|
# Check executable exists
|
||||||
if [ ! -x "$CHESSD_EXEC" ]; then
|
if [ ! -x "$CHESS_SERVER_EXEC" ]; then
|
||||||
echo -e "${RED}Error: chessd executable not found or not executable: $CHESSD_EXEC${NC}"
|
echo -e "${RED}Error: chess-server executable not found or not executable: $CHESS_SERVER_EXEC${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ fi
|
|||||||
# Start tests
|
# Start tests
|
||||||
print_header "Database & User Management Test Suite"
|
print_header "Database & User Management Test Suite"
|
||||||
echo "Server: $BASE_URL"
|
echo "Server: $BASE_URL"
|
||||||
echo "Executable: $CHESSD_EXEC"
|
echo "Executable: $CHESS_SERVER_EXEC"
|
||||||
echo "Test Database (server-managed): $TEST_DB"
|
echo "Test Database (server-managed): $TEST_DB"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@ -154,11 +154,11 @@ print_header "SECTION 1: CLI User Operations"
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
test_case "1.1: database initialization"
|
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)
|
# Create testuser1 first (not charlie)
|
||||||
test_case "1.2: Add First User via CLI"
|
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)
|
-email "testuser1@test.com" -password "TestPass123" 2>&1)
|
||||||
if echo "$OUTPUT" | grep -qi "User created successfully"; then
|
if echo "$OUTPUT" | grep -qi "User created successfully"; then
|
||||||
echo -e "${GREEN} ✓ User created: testuser1${NC}"
|
echo -e "${GREEN} ✓ User created: testuser1${NC}"
|
||||||
@ -169,7 +169,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
test_case "1.3: Add Second User"
|
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)
|
-password "TestPass456" 2>&1)
|
||||||
if echo "$OUTPUT" | grep -qi "User created successfully"; then
|
if echo "$OUTPUT" | grep -qi "User created successfully"; then
|
||||||
echo -e "${GREEN} ✓ User created: testuser2${NC}"
|
echo -e "${GREEN} ✓ User created: testuser2${NC}"
|
||||||
@ -181,7 +181,7 @@ fi
|
|||||||
|
|
||||||
# Now test duplicate prevention with an existing user
|
# Now test duplicate prevention with an existing user
|
||||||
test_case "1.4: Duplicate Username Prevention"
|
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"
|
"Duplicate username rejected"
|
||||||
|
|
||||||
test_case "1.5: Login with Case-Insensitive Username (ALICE)"
|
test_case "1.5: Login with Case-Insensitive Username (ALICE)"
|
||||||
@ -197,11 +197,11 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
test_case "1.6: Update User Email"
|
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"
|
"Email update"
|
||||||
|
|
||||||
test_case "1.7: Update User Password"
|
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"
|
"Password update"
|
||||||
|
|
||||||
test_case "2.1: Health Check"
|
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
|
# Now TEST_PASS2_NEW is defined, this test should work
|
||||||
test_case "5.1: Update User Password via CLI for 'bob'"
|
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'"
|
"CLI password update for '$TEST_USER2'"
|
||||||
|
|
||||||
test_case "5.2: Login with NEW Password for 'bob'"
|
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'"
|
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"
|
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"
|
"Add user '$TEST_USER_CLI' for hash test"
|
||||||
|
|
||||||
test_case "5.5: CLI rejects unsupported hash format"
|
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"
|
"Unsupported bcrypt hash rejected by CLI"
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@ -419,11 +419,11 @@ fi
|
|||||||
|
|
||||||
test_case "6.3: Delete User"
|
test_case "6.3: Delete User"
|
||||||
# First, get a user to delete
|
# 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)
|
-password "TempPass123" 2>&1)
|
||||||
TEMP_ID=$(echo "$OUTPUT" | grep "ID:" | awk '{print $2}')
|
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"
|
"User deletion by username"
|
||||||
|
|
||||||
# Verify deletion
|
# Verify deletion
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# test-longpoll.sh - Test long-polling functionality
|
# FILE: lixenwraith/chess/test/test-longpoll.sh
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user