v0.16.0 import PGN, delete games, two-layout web client
test / test (push) Canceled after 0s
test / e2e (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-10-19 21:02:39 -04:00
1 parent e1fa5242e3
commit 664cf9654e
34 files changed
+2007 -150

No files matched your search

+94 -1
View File
@@ -1,6 +1,7 @@
package cli
import (
"context"
"database/sql"
"errors"
"flag"
@@ -26,7 +27,7 @@ const dsnEnv = "CHESS_DSN"
// Run is the entry point for the CLI mini-app
func Run(args []string) error {
if len(args) == 0 {
return fmt.Errorf("subcommand required: init, delete, query, pgn, verify, user")
return fmt.Errorf("subcommand required: init, delete, query, pgn, verify, game, user")
}
switch args[0] {
@@ -40,6 +41,11 @@ func Run(args []string) error {
return runPGN(args[1:])
case "verify":
return runVerify(args[1:])
case "game":
if len(args) < 2 {
return fmt.Errorf("game subcommand required: delete, import")
}
return runGame(args[1], args[2:])
case "user":
if len(args) < 2 {
return fmt.Errorf("user subcommand required: add, delete, set-password, set-hash, set-email, set-username, reset-link, list")
@@ -268,6 +274,93 @@ func runVerify(args []string) error {
return nil
}
func runGame(subcommand string, args []string) error {
switch subcommand {
case "delete":
return runGameDelete(args)
case "import":
return runGameImport(args)
default:
return fmt.Errorf("unknown game subcommand: %s", subcommand)
}
}
// runGameDelete removes a stored game and its moves outright, whoever holds
// it. A live copy in a running server keeps playing in memory until it is
// unloaded; its later writes find no row and are dropped.
func runGameDelete(args []string) error {
fs, dsn := newFlagSet("game delete")
gameID := fs.String("gameId", "", "Game ID or unique prefix of at least 8 hex digits (required)")
confirm := fs.Bool("confirm", false, "Confirm deleting the game and its moves")
if err := fs.Parse(args); err != nil {
return err
}
if *gameID == "" {
return errors.New("-gameId is required")
}
if !*confirm {
return errors.New("refusing to delete a game without -confirm")
}
store, err := openStore(*dsn, true)
if err != nil {
return err
}
defer store.Close()
id, err := resolveGameID(store, *gameID)
if err != nil {
return err
}
if err := store.DeleteGames([]string{id}); err != nil {
return fmt.Errorf("delete game: %w", err)
}
if err := store.Flush(context.Background()); err != nil {
return fmt.Errorf("delete game: %w", err)
}
fmt.Printf("Deleted game %s\n", id)
return nil
}
// runGameImport stores a PGN file as a game of the named account, as the
// API's import does: every move is checked by the rules core.
func runGameImport(args []string) error {
fs, dsn := newFlagSet("game import")
file := fs.String("file", "", "PGN file holding one game (required)")
username := fs.String("username", "", "Account that will own the game (required)")
if err := fs.Parse(args); err != nil {
return err
}
if *file == "" || *username == "" {
return errors.New("-file and -username are required")
}
text, err := os.ReadFile(*file)
if err != nil {
return err
}
store, err := openStore(*dsn, true)
if err != nil {
return err
}
defer store.Close()
ownerID, err := lookupUser(store, *username, "")
if err != nil {
return err
}
svc, err := service.New(store, []byte(strings.Repeat("x", 32))) // no tokens are minted here
if err != nil {
return err
}
imported, err := svc.ImportGame(ownerID, string(text))
if err != nil {
return err
}
fmt.Printf("Imported game %s (%d plies) for %s\n", imported.GameID, imported.Plies, *username)
return nil
}
func resolveGameID(store *storage.Store, prefix string) (string, error) {
id, err := store.ResolveGameID(prefix)
if errors.Is(err, sql.ErrNoRows) {