From 3389c07e002e02197318f29103063deb2e1711d4 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:49:39 +0100 Subject: [PATCH 01/37] Add elicitation POC --- .../src/handlers/elicitation-handlers.ts | 322 ++++++++++++++++++ mcp-server/src/handlers/tool-handlers.ts | 94 ++++- mcp-server/src/server.ts | 3 +- mcp-server/src/utils/http-client.ts | 7 +- 4 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 mcp-server/src/handlers/elicitation-handlers.ts diff --git a/mcp-server/src/handlers/elicitation-handlers.ts b/mcp-server/src/handlers/elicitation-handlers.ts new file mode 100644 index 0000000..0154bea --- /dev/null +++ b/mcp-server/src/handlers/elicitation-handlers.ts @@ -0,0 +1,322 @@ +/** + * MCP Elicitation handlers for user input collection + * Provides structured ways to gather user preferences and decisions + */ + +export interface ElicitationResult { + action: "accept" | "decline" | "cancel" + content?: Record +} + +/** + * Game creation preferences elicitation + */ +export async function elicitGameCreationPreferences( + server: any, + gameType: string, + existingArgs?: Record +): Promise { + const schemas = { + 'tic-tac-toe': { + type: "object", + properties: { + difficulty: { + type: "string", + enum: ["easy", "medium", "hard"], + title: "AI Difficulty Level", + description: "How challenging should the AI opponent be?" + }, + playerSymbol: { + type: "string", + enum: ["X", "O"], + title: "Your Symbol", + description: "Do you want to be X (goes first) or O (goes second)?" + }, + playerName: { + type: "string", + title: "Player Name", + description: "What should we call you in the game?", + default: "Player" + } + }, + required: ["difficulty"] + }, + 'rock-paper-scissors': { + type: "object", + properties: { + difficulty: { + type: "string", + enum: ["easy", "medium", "hard"], + title: "AI Difficulty Level", + description: "How smart should the AI be at pattern recognition?" + }, + maxRounds: { + type: "number", + minimum: 1, + maximum: 10, + title: "Number of Rounds", + description: "How many rounds should we play?", + default: 3 + }, + playerName: { + type: "string", + title: "Player Name", + description: "What should we call you?", + default: "Player" + } + }, + required: ["difficulty"] + } + } + + const schema = schemas[gameType as keyof typeof schemas] + if (!schema) { + throw new Error(`No elicitation schema defined for game type: ${gameType}`) + } + + const message = `Let's set up your ${gameType.replace('-', ' ')} game! 🎮\n\nI'll need a few preferences to customize your experience:` + + try { + const result = await server.elicitInput({ + message, + requestedSchema: schema + }) + + return result + } catch (error) { + console.error('Elicitation failed:', error) + // Return default preferences if elicitation fails + return { + action: "accept", + content: { + difficulty: existingArgs?.aiDifficulty || "medium", + playerName: existingArgs?.playerName || "Player", + ...(gameType === 'rock-paper-scissors' && { maxRounds: 3 }), + ...(gameType === 'tic-tac-toe' && { playerSymbol: "X" }) + } + } + } +} + +/** + * Mid-game decision elicitation + */ +export async function elicitMidGameDecision( + server: any, + context: { + gameType: string + gameId: string + situation: string + options: Array<{ value: string; label: string; description?: string }> + } +): Promise { + const { gameType, gameId, situation, options } = context + + const schema = { + type: "object", + properties: { + choice: { + type: "string", + enum: options.map(opt => opt.value), + enumNames: options.map(opt => opt.label), + title: "Your Choice", + description: "What would you like to do?" + }, + feedback: { + type: "string", + title: "Any feedback? (Optional)", + description: "Let me know if you have any thoughts about the game so far" + } + }, + required: ["choice"] + } + + const message = `🤔 **${gameType.replace('-', ' ')} Game Decision**\n\n${situation}\n\nWhat would you like to do?` + + try { + return await server.elicitInput({ + message, + requestedSchema: schema + }) + } catch (error) { + console.error('Mid-game elicitation failed:', error) + // Return first option as default + return { + action: "accept", + content: { choice: options[0]?.value || "continue" } + } + } +} + +/** + * Game completion feedback elicitation + */ +export async function elicitGameCompletionFeedback( + server: any, + context: { + gameType: string + gameId: string + result: 'win' | 'loss' | 'draw' + aiDifficulty: string + } +): Promise { + const { gameType, result, aiDifficulty } = context + + const resultMessages = { + win: "🎉 Congratulations! You won!", + loss: "😅 Good game! The AI won this time.", + draw: "🤝 It's a draw! Well played by both sides." + } + + const schema = { + type: "object", + properties: { + difficultyFeedback: { + type: "string", + enum: ["too_easy", "just_right", "too_hard"], + enumNames: ["Too Easy", "Just Right", "Too Hard"], + title: "How was the difficulty?", + description: `The AI was set to ${aiDifficulty} difficulty` + }, + playAgain: { + type: "boolean", + title: "Play another game?", + description: "Would you like to start a new game?" + }, + gameTypeForNext: { + type: "string", + enum: ["same", "tic-tac-toe", "rock-paper-scissors"], + enumNames: ["Same Game", "Tic-Tac-Toe", "Rock Paper Scissors"], + title: "If playing again, which game?", + description: "Choose the game type for your next match" + }, + comments: { + type: "string", + title: "Any comments? (Optional)", + description: "Share your thoughts about the game experience" + } + }, + required: ["difficultyFeedback", "playAgain"] + } + + const message = `${resultMessages[result]}\n\n**Game Complete: ${gameType.replace('-', ' ')}**\n\nI'd love to get your feedback to improve future games:` + + try { + return await server.elicitInput({ + message, + requestedSchema: schema + }) + } catch (error) { + console.error('Completion feedback elicitation failed:', error) + return { + action: "decline", + content: {} + } + } +} + +/** + * Strategy hint elicitation + */ +export async function elicitStrategyPreference( + server: any, + context: { + gameType: string + gameId: string + availableHints: string[] + currentSituation: string + } +): Promise { + const { gameType, availableHints, currentSituation } = context + + const schema = { + type: "object", + properties: { + wantHint: { + type: "boolean", + title: "Would you like a strategy hint?", + description: "I can provide some strategic advice for this situation" + }, + hintType: { + type: "string", + enum: ["beginner", "intermediate", "advanced"], + enumNames: ["Basic Tips", "Strategic Insights", "Advanced Analysis"], + title: "What level of hint?", + description: "Choose the depth of strategic advice" + }, + explainMoves: { + type: "boolean", + title: "Explain possible moves?", + description: "Would you like me to analyze the available options?" + } + }, + required: ["wantHint"] + } + + const message = `🧠 **Strategy Assistance Available**\n\n**Current situation:** ${currentSituation}\n\nI can provide strategic guidance if you'd like:` + + try { + return await server.elicitInput({ + message, + requestedSchema: schema + }) + } catch (error) { + console.error('Strategy elicitation failed:', error) + return { + action: "decline", + content: { wantHint: false } + } + } +} + +/** + * Error recovery elicitation + */ +export async function elicitErrorRecovery( + server: any, + context: { + gameType: string + gameId: string + error: string + recoveryOptions: Array<{ value: string; label: string; description: string }> + } +): Promise { + const { gameType, error, recoveryOptions } = context + + const schema = { + type: "object", + properties: { + action: { + type: "string", + enum: recoveryOptions.map(opt => opt.value), + enumNames: recoveryOptions.map(opt => opt.label), + title: "How should we handle this?", + description: "Choose your preferred recovery option" + }, + reportIssue: { + type: "boolean", + title: "Report this issue for improvement?", + description: "Help us improve by reporting this problem" + } + }, + required: ["action"] + } + + const message = `⚠️ **${gameType.replace('-', ' ')} Game Issue**\n\n**Problem:** ${error}\n\nHow would you like to proceed?` + + try { + return await server.elicitInput({ + message, + requestedSchema: schema + }) + } catch (error) { + console.error('Error recovery elicitation failed:', error) + return { + action: "accept", + content: { + action: recoveryOptions[0]?.value || "retry", + reportIssue: false + } + } + } +} diff --git a/mcp-server/src/handlers/tool-handlers.ts b/mcp-server/src/handlers/tool-handlers.ts index f0bbe51..3ec57cc 100644 --- a/mcp-server/src/handlers/tool-handlers.ts +++ b/mcp-server/src/handlers/tool-handlers.ts @@ -3,6 +3,7 @@ */ import { playGame, analyzeGame, waitForPlayerMove, createGame } from './game-operations.js' +import { elicitGameCreationPreferences } from './elicitation-handlers.js' export const TOOL_DEFINITIONS = [ { @@ -127,12 +128,35 @@ export const TOOL_DEFINITIONS = [ required: [], }, }, + { + name: 'create_tic_tac_toe_game_interactive', + description: 'Create a new Tic-Tac-Toe game with interactive setup. This will ask you for preferences like difficulty and symbol choice.', + inputSchema: { + type: 'object', + properties: { + gameId: { + type: 'string', + description: 'Optional custom game ID. If not provided, a random UUID will be generated.', + }, + }, + required: [], + }, + }, + { + name: 'create_rock_paper_scissors_game_interactive', + description: 'Create a new Rock Paper Scissors game with interactive setup. This will ask you for preferences like difficulty and number of rounds.', + inputSchema: { + type: 'object', + properties: {}, + required: [], + }, + }, ] /** * Handle tool execution */ -export async function handleToolCall(name: string, args: any) { +export async function handleToolCall(name: string, args: any, server?: any) { try { switch (name) { case 'play_tic_tac_toe': @@ -189,6 +213,13 @@ export async function handleToolCall(name: string, args: any) { } = args return await createGame('rock-paper-scissors', rpsPlayerName, undefined, rpsAiDifficulty) + case 'create_tic_tac_toe_game_interactive': + const { gameId: interactiveTTTGameId } = args + return await createGameInteractive('tic-tac-toe', interactiveTTTGameId, server) + + case 'create_rock_paper_scissors_game_interactive': + return await createGameInteractive('rock-paper-scissors', undefined, server) + default: throw new Error(`Unknown tool: ${name}`) } @@ -196,3 +227,64 @@ export async function handleToolCall(name: string, args: any) { throw error } } + +/** + * Create game with interactive elicitation + */ +async function createGameInteractive(gameType: string, gameId?: string, server?: any) { + if (!server) { + // Fallback to regular creation if no server for elicitation + return await createGame(gameType, 'Player', gameId, 'medium') + } + + try { + // Elicit user preferences + const elicitationResult = await elicitGameCreationPreferences(server, gameType, { + gameId, + playerName: 'Player', + aiDifficulty: 'medium' + }) + + if (elicitationResult.action === 'decline' || elicitationResult.action === 'cancel') { + return { + gameId: null, + gameType, + message: `🚫 Game creation ${elicitationResult.action}d by user`, + status: 'cancelled', + action: elicitationResult.action + } + } + + if (elicitationResult.action === 'accept' && elicitationResult.content) { + const { difficulty, playerName, playerSymbol, maxRounds } = elicitationResult.content + + // Prepare game creation parameters + const finalPlayerName = playerName || 'Player' + const finalDifficulty = difficulty || 'medium' + + // Create the game with elicited preferences + const gameResult = await createGame(gameType, finalPlayerName, gameId, finalDifficulty) + + // Add elicitation information to the response + gameResult.elicitation = { + preferences: elicitationResult.content, + message: '🎮 Game created with your custom preferences!' + } + + // Add game-specific messages + if (gameType === 'tic-tac-toe' && playerSymbol) { + gameResult.message += ` You are playing as ${playerSymbol}.` + } + if (gameType === 'rock-paper-scissors' && maxRounds) { + gameResult.message += ` Playing ${maxRounds} rounds.` + } + + return gameResult + } + } catch (error) { + console.warn('Interactive game creation failed, falling back to defaults:', error) + } + + // Fallback to regular creation + return await createGame(gameType, 'Player', gameId, 'medium') +} diff --git a/mcp-server/src/server.ts b/mcp-server/src/server.ts index f3a0699..aec2712 100644 --- a/mcp-server/src/server.ts +++ b/mcp-server/src/server.ts @@ -26,6 +26,7 @@ const server = new Server( tools: {}, resources: {}, prompts: {}, + sampling: {}, }, } ) @@ -70,7 +71,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { // conversational flow between moves. try { - const result = await handleToolCall(name, args) + const result = await handleToolCall(name, args, server) return { content: [ { diff --git a/mcp-server/src/utils/http-client.ts b/mcp-server/src/utils/http-client.ts index 105e6ab..4535eef 100644 --- a/mcp-server/src/utils/http-client.ts +++ b/mcp-server/src/utils/http-client.ts @@ -44,7 +44,8 @@ export async function createGameViaAPI( gameType: string, playerName: string, gameId?: string, - aiDifficulty?: string + aiDifficulty?: string, + gameSpecificOptions?: Record ) { try { const data: any = { playerName } @@ -54,6 +55,10 @@ export async function createGameViaAPI( if (aiDifficulty) { data.aiDifficulty = aiDifficulty } + // Merge any game-specific options + if (gameSpecificOptions) { + Object.assign(data, gameSpecificOptions) + } return await httpPost(`${WEB_API_BASE}/api/games/${gameType}`, data) } catch (error) { console.error(`Error creating ${gameType} game via API:`, error) From e2ab29c537f315ef7f9628a828f81abd508dfc4a Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:43:33 +0100 Subject: [PATCH 02/37] Update so that either player can go first --- .../src/handlers/elicitation-handlers.ts | 3 +- mcp-server/src/handlers/game-operations.ts | 5 ++- mcp-server/src/handlers/tool-handlers.ts | 29 +++++++++++++-- mcp-server/src/server.ts | 2 +- shared/src/games/rock-paper-scissors.ts | 2 +- shared/src/games/tic-tac-toe.ts | 37 +++++++++++++++---- shared/src/types/game.ts | 2 +- web/src/app/api/games/tic-tac-toe/route.ts | 15 +++++++- web/src/app/games/tic-tac-toe/page.tsx | 19 +++++++++- 9 files changed, 93 insertions(+), 21 deletions(-) diff --git a/mcp-server/src/handlers/elicitation-handlers.ts b/mcp-server/src/handlers/elicitation-handlers.ts index 0154bea..a30120e 100644 --- a/mcp-server/src/handlers/elicitation-handlers.ts +++ b/mcp-server/src/handlers/elicitation-handlers.ts @@ -30,7 +30,8 @@ export async function elicitGameCreationPreferences( type: "string", enum: ["X", "O"], title: "Your Symbol", - description: "Do you want to be X (goes first) or O (goes second)?" + description: "Do you want to be X (goes first) or O (goes second)?", + default: "X" }, playerName: { type: "string", diff --git a/mcp-server/src/handlers/game-operations.ts b/mcp-server/src/handlers/game-operations.ts index c5e61eb..e00a8a8 100644 --- a/mcp-server/src/handlers/game-operations.ts +++ b/mcp-server/src/handlers/game-operations.ts @@ -318,7 +318,8 @@ export async function createGame( gameType: string, playerName: string = 'Player', gameId?: string, - aiDifficulty: string = 'medium' + aiDifficulty: string = 'medium', + gameSpecificOptions?: Record ) { // Check if game already exists (for games that support custom IDs) if (gameId && gameType === 'tic-tac-toe') { @@ -341,7 +342,7 @@ export async function createGame( } // Create new game via API - const gameSession = await createGameViaAPI(gameType, playerName, gameId, aiDifficulty) + const gameSession = await createGameViaAPI(gameType, playerName, gameId, aiDifficulty, gameSpecificOptions) const response: any = { gameId: gameSession.gameState.id, diff --git a/mcp-server/src/handlers/tool-handlers.ts b/mcp-server/src/handlers/tool-handlers.ts index 3ec57cc..b13e39b 100644 --- a/mcp-server/src/handlers/tool-handlers.ts +++ b/mcp-server/src/handlers/tool-handlers.ts @@ -103,6 +103,12 @@ export const TOOL_DEFINITIONS = [ description: 'AI difficulty level', default: 'medium', }, + playerSymbol: { + type: 'string', + enum: ['X', 'O'], + description: 'Your symbol: X (goes first) or O (goes second)', + default: 'X', + }, }, required: [], }, @@ -202,9 +208,12 @@ export async function handleToolCall(name: string, args: any, server?: any) { const { playerName: ticTacToePlayerName = 'Player', gameId: ticTacToeNewGameId, - aiDifficulty: ticTacToeAiDifficulty = 'medium' + aiDifficulty: ticTacToeAiDifficulty = 'medium', + playerSymbol: ticTacToePlayerSymbol = 'X' } = args - return await createGame('tic-tac-toe', ticTacToePlayerName, ticTacToeNewGameId, ticTacToeAiDifficulty) + + const ticTacToeGameOptions = ticTacToePlayerSymbol ? { playerSymbol: ticTacToePlayerSymbol } : undefined + return await createGame('tic-tac-toe', ticTacToePlayerName, ticTacToeNewGameId, ticTacToeAiDifficulty, ticTacToeGameOptions) case 'create_rock_paper_scissors_game': const { @@ -262,8 +271,17 @@ async function createGameInteractive(gameType: string, gameId?: string, server?: const finalPlayerName = playerName || 'Player' const finalDifficulty = difficulty || 'medium' + // Prepare game-specific options + const gameSpecificOptions: Record = {} + if (gameType === 'tic-tac-toe' && playerSymbol) { + gameSpecificOptions.playerSymbol = playerSymbol + } + if (gameType === 'rock-paper-scissors' && maxRounds) { + gameSpecificOptions.maxRounds = maxRounds + } + // Create the game with elicited preferences - const gameResult = await createGame(gameType, finalPlayerName, gameId, finalDifficulty) + const gameResult = await createGame(gameType, finalPlayerName, gameId, finalDifficulty, gameSpecificOptions) // Add elicitation information to the response gameResult.elicitation = { @@ -274,6 +292,11 @@ async function createGameInteractive(gameType: string, gameId?: string, server?: // Add game-specific messages if (gameType === 'tic-tac-toe' && playerSymbol) { gameResult.message += ` You are playing as ${playerSymbol}.` + if (playerSymbol === 'X') { + gameResult.message += ' You go first!' + } else { + gameResult.message += ' AI goes first!' + } } if (gameType === 'rock-paper-scissors' && maxRounds) { gameResult.message += ` Playing ${maxRounds} rounds.` diff --git a/mcp-server/src/server.ts b/mcp-server/src/server.ts index aec2712..399a2c1 100644 --- a/mcp-server/src/server.ts +++ b/mcp-server/src/server.ts @@ -26,7 +26,7 @@ const server = new Server( tools: {}, resources: {}, prompts: {}, - sampling: {}, + elicitation: {}, }, } ) diff --git a/shared/src/games/rock-paper-scissors.ts b/shared/src/games/rock-paper-scissors.ts index c40ca53..a1fbc09 100644 --- a/shared/src/games/rock-paper-scissors.ts +++ b/shared/src/games/rock-paper-scissors.ts @@ -221,7 +221,7 @@ export class RockPaperScissorsGame implements Game { * - Game status set to 'playing' * - Current round set to 0 */ - getInitialState(players: Player[]): RPSGameState { + getInitialState(players: Player[], options?: any): RPSGameState { const maxRounds = 3; // Best of 3 const rounds = Array.from({ length: maxRounds }, () => ({})); diff --git a/shared/src/games/tic-tac-toe.ts b/shared/src/games/tic-tac-toe.ts index b23e2ab..9cbb0c7 100644 --- a/shared/src/games/tic-tac-toe.ts +++ b/shared/src/games/tic-tac-toe.ts @@ -189,32 +189,53 @@ export class TicTacToeGame implements Game { /** * Creates the initial game state for a new tic-tac-toe game * - * @param players - Array of exactly 2 players (first player gets X, second gets O) - * @returns Initial TicTacToeGameState with empty board and first player's turn + * @param players - Array of exactly 2 players + * @param options - Optional game configuration + * @returns Initial TicTacToeGameState with empty board * * @description * Sets up a new game with: * - Empty 3x3 board (all cells null) - * - Player 1 assigned 'X' symbol and goes first - * - Player 2 assigned 'O' symbol + * - Player symbol assignment (X always goes first, O goes second) + * - Configurable turn order (who gets X and goes first) * - Game status set to 'playing' * - Timestamps initialized to current time */ - getInitialState(players: Player[]): TicTacToeGameState { + getInitialState(players: Player[], options?: { firstPlayerId?: string }): TicTacToeGameState { const board: Board = [ [null, null, null], [null, null, null], [null, null, null] ]; + // Determine who gets X (goes first) and who gets O (goes second) + let firstPlayer: Player; + let secondPlayer: Player; + + if (options?.firstPlayerId) { + const firstPlayerIndex = players.findIndex(p => p.id === options.firstPlayerId); + if (firstPlayerIndex !== -1) { + firstPlayer = players[firstPlayerIndex]; + secondPlayer = players[1 - firstPlayerIndex]; + } else { + // Fallback to default order if specified player not found + firstPlayer = players[0]; + secondPlayer = players[1]; + } + } else { + // Default: first player in array goes first + firstPlayer = players[0]; + secondPlayer = players[1]; + } + const playerSymbols: Record = {}; - playerSymbols[players[0].id] = 'X'; - playerSymbols[players[1].id] = 'O'; + playerSymbols[firstPlayer.id] = 'X'; + playerSymbols[secondPlayer.id] = 'O'; return { id: crypto.randomUUID(), players, - currentPlayerId: players[0].id, + currentPlayerId: firstPlayer.id, status: 'playing', createdAt: new Date(), updatedAt: new Date(), diff --git a/shared/src/types/game.ts b/shared/src/types/game.ts index 869cbc7..b23a6b0 100644 --- a/shared/src/types/game.ts +++ b/shared/src/types/game.ts @@ -36,7 +36,7 @@ export interface Game { applyMove(gameState: TGameState, move: TMove, playerId: PlayerId): TGameState; checkGameEnd(gameState: TGameState): GameResult | null; getValidMoves(gameState: TGameState, playerId: PlayerId): TMove[]; - getInitialState(players: Player[]): TGameState; + getInitialState(players: Player[], options?: any): TGameState; } export type GameType = 'tic-tac-toe' | 'rock-paper-scissors'; diff --git a/web/src/app/api/games/tic-tac-toe/route.ts b/web/src/app/api/games/tic-tac-toe/route.ts index 3188c40..137fd3d 100644 --- a/web/src/app/api/games/tic-tac-toe/route.ts +++ b/web/src/app/api/games/tic-tac-toe/route.ts @@ -8,14 +8,25 @@ const ticTacToeGame = new TicTacToeGame() export async function POST(request: NextRequest) { try { - const { playerName, gameId, aiDifficulty } = await request.json() + const { playerName, gameId, aiDifficulty, playerSymbol } = await request.json() const players: Player[] = [ { id: 'player1', name: playerName || 'Player', isAI: false }, { id: 'ai', name: 'AI', isAI: true } ] - const gameState = ticTacToeGame.getInitialState(players) + // Determine who goes first based on symbol choice + // X always goes first, O goes second + let options: { firstPlayerId?: string } | undefined; + if (playerSymbol === 'O') { + // Player chose O, so AI (who gets X) goes first + options = { firstPlayerId: 'ai' }; + } else { + // Player chose X (default) or no preference, so player goes first + options = { firstPlayerId: 'player1' }; + } + + const gameState = ticTacToeGame.getInitialState(players, options) // Use custom gameId if provided if (gameId) { diff --git a/web/src/app/games/tic-tac-toe/page.tsx b/web/src/app/games/tic-tac-toe/page.tsx index e8694b5..27302f7 100644 --- a/web/src/app/games/tic-tac-toe/page.tsx +++ b/web/src/app/games/tic-tac-toe/page.tsx @@ -17,6 +17,7 @@ export default function TicTacToePage() { const [showCreateForm, setShowCreateForm] = useState(false) const [showJoinForm, setShowJoinForm] = useState(false) const [aiDifficulty, setAiDifficulty] = useState<'easy' | 'medium' | 'hard'>('medium') + const [playerSymbol, setPlayerSymbol] = useState<'X' | 'O'>('X') const [gamesToShow, setGamesToShow] = useState(5) const [showDeleteModal, setShowDeleteModal] = useState(false) const [gameToDelete, setGameToDelete] = useState(null) @@ -79,9 +80,10 @@ export default function TicTacToePage() { setError(null) try { - const body: { playerName: string; gameId?: string; aiDifficulty: string } = { + const body: { playerName: string; gameId?: string; aiDifficulty: string; playerSymbol: string } = { playerName: 'Player', - aiDifficulty + aiDifficulty, + playerSymbol } if (customGameId) { body.gameId = customGameId @@ -433,6 +435,19 @@ export default function TicTacToePage() { +
+ + +
setAiDifficulty(e.target.value as 'easy' | 'medium' | 'hard')} + onChange={(e) => setAiDifficulty(e.target.value as Difficulty)} className="w-full px-4 py-3 bg-white/60 dark:bg-slate-700/60 border border-slate-200 dark:border-slate-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all duration-200" > diff --git a/web/src/test-utils/api-route-mocks.ts b/web/src/test-utils/api-route-mocks.ts index df30418..8b9304a 100644 --- a/web/src/test-utils/api-route-mocks.ts +++ b/web/src/test-utils/api-route-mocks.ts @@ -7,6 +7,7 @@ import { vi } from 'vitest' import { NextRequest } from 'next/server' +import type { GameType } from '@turn-based-mcp/shared' import { createSharedGameMocks, createStorageMocks, @@ -19,7 +20,7 @@ import { * Setup standard API route mocks for a specific game type * This consolidates the common mock setup pattern used across API route tests */ -export function setupAPIRouteMocks(gameType: 'tic-tac-toe' | 'rock-paper-scissors') { +export function setupAPIRouteMocks(gameType: GameType) { const gameClass = gameType === 'tic-tac-toe' ? 'TicTacToeGame' : 'RockPaperScissorsGame' // Create shared game mocks @@ -42,7 +43,7 @@ export function setupAPIRouteMocks(gameType: 'tic-tac-toe' | 'rock-paper-scissor * Setup mocks for move route testing * Handles the slightly different mock setup needed for move routes */ -export function setupMoveRouteMocks(gameType: 'tic-tac-toe' | 'rock-paper-scissors') { +export function setupMoveRouteMocks(gameType: GameType) { const gameClass = gameType === 'tic-tac-toe' ? 'TicTacToeGame' : 'RockPaperScissorsGame' // Create game mock From fc8b73c4dc12ceeb7d193ff107e1b0c578f13828 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:10:39 +0100 Subject: [PATCH 16/37] Add type to remove ide warnings --- package-lock.json | 466 ++++++++++++++++++++++++++++++++++++++++++++ shared/package.json | 2 + 2 files changed, 468 insertions(+) diff --git a/package-lock.json b/package-lock.json index 8239aca..0422182 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1681,6 +1681,85 @@ "node": ">=18.0.0" } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", + "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", @@ -2282,6 +2361,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2846,6 +2932,79 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2900,6 +3059,30 @@ "sqlite3": "*" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.38.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", @@ -4387,6 +4570,22 @@ "node": ">=10" } }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -5658,6 +5857,24 @@ "node": ">=6" } }, + "node_modules/expect": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.0.5", + "@jest/get-type": "30.0.1", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -7061,6 +7278,220 @@ "node": ">= 0.4" } }, + "node_modules/jest-diff": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", + "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", + "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.5", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jiti": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", @@ -9620,6 +10051,16 @@ "node": ">=18" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -9734,6 +10175,29 @@ "dev": true, "license": "MIT" }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -11125,6 +11589,8 @@ "sqlite3": "^5.1.7" }, "devDependencies": { + "@jest/types": "30.0.5", + "@types/jest": "^30.0.0", "@vitest/ui": "^3.2.4", "fix-esm-import-path": "^1.10.3", "typescript": "^5.5.0", diff --git a/shared/package.json b/shared/package.json index a1b837d..515c1de 100644 --- a/shared/package.json +++ b/shared/package.json @@ -16,6 +16,8 @@ "test:ui": "vitest --ui" }, "devDependencies": { + "@jest/types": "30.0.5", + "@types/jest": "^30.0.0", "@vitest/ui": "^3.2.4", "fix-esm-import-path": "^1.10.3", "typescript": "^5.5.0", From 3920441d54b8608a17f348423ced30431837928a Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:26:06 +0100 Subject: [PATCH 17/37] Fix RPS game complete bug, and add number of rounds support --- mcp-server/tsconfig.json | 2 +- shared/src/games/rock-paper-scissors.ts | 9 +++--- .../api/games/rock-paper-scissors/route.ts | 4 +-- .../app/games/rock-paper-scissors/page.tsx | 30 +++++++++++++++---- web/src/components/games/RPSGameBoard.tsx | 5 +++- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json index b49cd9b..031bbe4 100644 --- a/mcp-server/tsconfig.json +++ b/mcp-server/tsconfig.json @@ -15,5 +15,5 @@ "allowSyntheticDefaultImports": true }, "include": ["src/**/*"], - "exclude": ["dist", "node_modules"] + "exclude": ["dist", "node_modules", "**/*.test.ts", "**/*.test.tsx"] } diff --git a/shared/src/games/rock-paper-scissors.ts b/shared/src/games/rock-paper-scissors.ts index a1fbc09..c0d779c 100644 --- a/shared/src/games/rock-paper-scissors.ts +++ b/shared/src/games/rock-paper-scissors.ts @@ -211,18 +211,19 @@ export class RockPaperScissorsGame implements Game { * Creates the initial game state for a new rock-paper-scissors game * * @param players - Array of exactly 2 players - * @returns Initial RPSGameState set up for a best-of-3 match + * @param options - Optional configuration including maxRounds + * @returns Initial RPSGameState set up for a configurable number of rounds * * @description * Sets up a new game with: - * - 3 empty rounds (best-of-3 format) + * - Configurable number of rounds (default: 3 for best-of-3 format) * - All scores initialized to 0 * - First player goes first * - Game status set to 'playing' * - Current round set to 0 */ - getInitialState(players: Player[], options?: any): RPSGameState { - const maxRounds = 3; // Best of 3 + getInitialState(players: Player[], options?: { maxRounds?: number }): RPSGameState { + const maxRounds = options?.maxRounds || 3; // Default to best of 3 const rounds = Array.from({ length: maxRounds }, () => ({})); const scores: Record = {}; diff --git a/web/src/app/api/games/rock-paper-scissors/route.ts b/web/src/app/api/games/rock-paper-scissors/route.ts index 2ab3a23..8693798 100644 --- a/web/src/app/api/games/rock-paper-scissors/route.ts +++ b/web/src/app/api/games/rock-paper-scissors/route.ts @@ -8,14 +8,14 @@ const rpsGame = new RockPaperScissorsGame() export async function POST(request: NextRequest) { try { - const { playerName, gameId, aiDifficulty } = await request.json() + const { playerName, gameId, aiDifficulty, maxRounds } = await request.json() const players: Player[] = [ { id: 'player1', name: playerName || 'Player', isAI: false }, { id: 'ai', name: 'AI', isAI: true } ] - const gameState = rpsGame.getInitialState(players) + const gameState = rpsGame.getInitialState(players, { maxRounds }) // Use custom gameId if provided if (gameId) { diff --git a/web/src/app/games/rock-paper-scissors/page.tsx b/web/src/app/games/rock-paper-scissors/page.tsx index 8984891..4b7fdae 100644 --- a/web/src/app/games/rock-paper-scissors/page.tsx +++ b/web/src/app/games/rock-paper-scissors/page.tsx @@ -6,7 +6,7 @@ import { GameInfoPanel } from '../../../components/games/GameInfoPanel' import { GameContainer, GameControls, ConfirmationModal } from '../../../components/ui' import { MCPAssistantPanel } from '../../../components/shared' import type { RPSGameState, RPSMove } from '@turn-based-mcp/shared' -import type { GameSession } from '@turn-based-mcp/shared' +import type { GameSession, Difficulty } from '@turn-based-mcp/shared' export default function RockPaperScissorsPage() { const [gameSession, setGameSession] = useState | null>(null) @@ -16,7 +16,8 @@ export default function RockPaperScissorsPage() { const [availableGames, setAvailableGames] = useState[]>([]) const [showCreateForm, setShowCreateForm] = useState(false) const [showJoinForm, setShowJoinForm] = useState(false) - const [aiDifficulty, setAiDifficulty] = useState<'easy' | 'medium' | 'hard'>('medium') + const [aiDifficulty, setAiDifficulty] = useState('medium') + const [maxRounds, setMaxRounds] = useState(3) const [gamesToShow, setGamesToShow] = useState(5) const [showDeleteModal, setShowDeleteModal] = useState(false) const [gameToDelete, setGameToDelete] = useState(null) @@ -79,9 +80,10 @@ export default function RockPaperScissorsPage() { setError(null) try { - const body: { playerName: string; gameId?: string; aiDifficulty: string } = { + const body: { playerName: string; gameId?: string; aiDifficulty: string; maxRounds: number } = { playerName: 'Player', - aiDifficulty + aiDifficulty, + maxRounds } if (customGameId) { body.gameId = customGameId @@ -280,7 +282,7 @@ export default function RockPaperScissorsPage() { <> 🔴 Hard - Advanced pattern recognition
+
+ + +
} // Empty sidebar for setup error={error} diff --git a/web/src/components/games/RPSGameBoard.tsx b/web/src/components/games/RPSGameBoard.tsx index caf67a0..b641415 100644 --- a/web/src/components/games/RPSGameBoard.tsx +++ b/web/src/components/games/RPSGameBoard.tsx @@ -108,7 +108,10 @@ export function RPSGameBoard({ gameState, onMove, disabled }: RPSGameBoardProps)

- Round {gameState.currentRound + 1} of {gameState.maxRounds} + {gameState.status === 'finished' ? + `Game Complete (${gameState.currentRound} of ${gameState.maxRounds} rounds played)` : + `Round ${gameState.currentRound + 1} of ${gameState.maxRounds}` + }

{gameState.status === 'playing' && ( From de6dddaae421dac032978101edc1521b854c8442 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:30:09 +0100 Subject: [PATCH 18/37] Fix MCP config --- .vscode/mcp.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index dab7a00..918a47b 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -22,5 +22,5 @@ "@modelcontextprotocol/server-sequential-thinking" ] } - } } + } } \ No newline at end of file From 6644741ef0d78ee3b9f97a88d77186715b06e355 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:01:22 +0100 Subject: [PATCH 19/37] Fix "smart" elicitation --- .vscode/mcp.json | 2 +- .../src/handlers/elicitation-handlers.ts | 53 +++++++++++++++---- mcp-server/src/handlers/game-operations.ts | 6 +-- mcp-server/src/handlers/resource-handlers.ts | 2 +- mcp-server/src/handlers/tool-handlers.ts | 30 +++++++++-- .../src/integration/integration.test.ts | 4 +- mcp-server/src/utils/http-client.ts | 6 +-- shared/src/index.ts | 1 - shared/src/testing/api-test-utils.ts | 2 +- shared/src/types/game.ts | 2 +- .../api/games/rock-paper-scissors/route.ts | 4 +- web/src/app/api/games/tic-tac-toe/route.ts | 4 +- .../app/games/rock-paper-scissors/page.tsx | 8 +-- web/src/app/games/tic-tac-toe/page.tsx | 12 ++--- 14 files changed, 96 insertions(+), 40 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 918a47b..1ea4125 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -2,7 +2,7 @@ "servers": { "turn-based-games": { "command": "node", - "args": ["dist/index.js"], + "args": ["dist/server.js"], "cwd": "./mcp-server" }, "playwright": { diff --git a/mcp-server/src/handlers/elicitation-handlers.ts b/mcp-server/src/handlers/elicitation-handlers.ts index bbe6c20..be5ed7f 100644 --- a/mcp-server/src/handlers/elicitation-handlers.ts +++ b/mcp-server/src/handlers/elicitation-handlers.ts @@ -72,19 +72,54 @@ export async function elicitGameCreationPreferences( } } - const schema = schemas[gameType as keyof typeof schemas] - if (!schema) { + const baseSchema = schemas[gameType as keyof typeof schemas] + if (!baseSchema) { throw new Error(`No elicitation schema defined for game type: ${gameType}`) } + // Filter out properties that are already provided + const filteredSchema: any = { ...baseSchema } + const filteredProperties: Record = { ...baseSchema.properties } + const filteredRequired = [...(baseSchema.required || [])] + + // Remove properties that already have values + if (existingArgs) { + Object.keys(existingArgs).forEach(key => { + if (existingArgs[key] !== undefined && existingArgs[key] !== null && existingArgs[key] !== '') { + delete filteredProperties[key] + // Remove from required array if present + const requiredIndex = filteredRequired.indexOf(key) + if (requiredIndex > -1) { + filteredRequired.splice(requiredIndex, 1) + } + } + }) + } + + filteredSchema.properties = filteredProperties + filteredSchema.required = filteredRequired + + // If no properties remain to be elicited, skip elicitation + if (Object.keys(filteredProperties).length === 0) { + return { + action: "accept", + content: existingArgs || {} + } + } + const message = `Let's set up your ${gameType.replace('-', ' ')} game! 🎮\n\nI'll need a few preferences to customize your experience:` try { const result = await server.elicitInput({ message, - requestedSchema: schema + requestedSchema: filteredSchema }) + // Merge the elicitation result with existing arguments + if (result.action === 'accept' && result.content) { + result.content = { ...existingArgs, ...result.content } + } + return result } catch (error) { console.error('Elicitation failed:', error) @@ -92,10 +127,10 @@ export async function elicitGameCreationPreferences( return { action: "accept", content: { - difficulty: existingArgs?.aiDifficulty || DEFAULT_AI_DIFFICULTY, + difficulty: existingArgs?.difficulty || DEFAULT_AI_DIFFICULTY, playerName: existingArgs?.playerName || DEFAULT_PLAYER_NAME, - ...(gameType === 'rock-paper-scissors' && { maxRounds: 3 }), - ...(gameType === 'tic-tac-toe' && { playerSymbol: "X" }) + ...(gameType === 'rock-paper-scissors' && { maxRounds: existingArgs?.maxRounds || 3 }), + ...(gameType === 'tic-tac-toe' && { playerSymbol: existingArgs?.playerSymbol || "X" }) } } } @@ -160,10 +195,10 @@ export async function elicitGameCompletionFeedback( gameType: string gameId: string result: 'win' | 'loss' | 'draw' - aiDifficulty: string + difficulty: string } ): Promise { - const { gameType, result, aiDifficulty } = context + const { gameType, result, difficulty } = context const resultMessages = { win: "🎉 Congratulations! You won!", @@ -179,7 +214,7 @@ export async function elicitGameCompletionFeedback( enum: ["too_easy", "just_right", "too_hard"], enumNames: ["Too Easy", "Just Right", "Too Hard"], title: "How was the difficulty?", - description: `The AI was set to ${aiDifficulty} difficulty` + description: `The AI was set to ${difficulty} difficulty` }, playAgain: { type: "boolean", diff --git a/mcp-server/src/handlers/game-operations.ts b/mcp-server/src/handlers/game-operations.ts index dd2ea86..04b9d0b 100644 --- a/mcp-server/src/handlers/game-operations.ts +++ b/mcp-server/src/handlers/game-operations.ts @@ -38,7 +38,7 @@ export async function playGame(gameType: string, gameId: string) { const gameSession = await readGameResource(gameType, gameId) // Use the difficulty stored in the game session, or fall back to medium - const difficulty = gameSession.aiDifficulty || 'medium' + const difficulty = gameSession.difficulty || 'medium' // Check if it's AI's turn if (gameSession.gameState.currentPlayerId !== 'ai') { @@ -319,7 +319,7 @@ export async function createGame( gameType: string, playerName: string = DEFAULT_PLAYER_NAME, gameId?: string, - aiDifficulty: string = DEFAULT_AI_DIFFICULTY, + difficulty: string = DEFAULT_AI_DIFFICULTY, gameSpecificOptions?: Record ) { // Check if game already exists (for games that support custom IDs) @@ -343,7 +343,7 @@ export async function createGame( } // Create new game via API - const gameSession = await createGameViaAPI(gameType, playerName, gameId, aiDifficulty, gameSpecificOptions) + const gameSession = await createGameViaAPI(gameType, playerName, gameId, difficulty, gameSpecificOptions) const response: any = { gameId: gameSession.gameState.id, diff --git a/mcp-server/src/handlers/resource-handlers.ts b/mcp-server/src/handlers/resource-handlers.ts index bb9682b..bd1fd03 100644 --- a/mcp-server/src/handlers/resource-handlers.ts +++ b/mcp-server/src/handlers/resource-handlers.ts @@ -109,7 +109,7 @@ export async function readResource(uri: string) { createdAt: game.gameState?.createdAt, updatedAt: game.gameState?.updatedAt, playerCount: Object.keys(game.gameState?.players || {}).length, - aiDifficulty: game.aiDifficulty + difficulty: game.difficulty })), totalGames: games.length, timestamp: new Date().toISOString() diff --git a/mcp-server/src/handlers/tool-handlers.ts b/mcp-server/src/handlers/tool-handlers.ts index 2f0d484..d8130ad 100644 --- a/mcp-server/src/handlers/tool-handlers.ts +++ b/mcp-server/src/handlers/tool-handlers.ts @@ -88,6 +88,26 @@ export const TOOL_DEFINITIONS = [ gameId: { type: 'string', description: 'Optional custom game ID. If not provided, a random UUID will be generated.' + }, + difficulty: { + type: 'string', + enum: DIFFICULTIES, + description: 'AI difficulty level (easy, medium, hard). If not provided, will be asked during setup.' + }, + playerName: { + type: 'string', + description: 'Your name in the game. If not provided, will be asked during setup.' + }, + playerSymbol: { + type: 'string', + enum: ['X', 'O'], + description: 'For tic-tac-toe: your symbol (X goes first, O goes second). If not provided, will be asked during setup.' + }, + maxRounds: { + type: 'number', + minimum: 1, + maximum: 10, + description: 'For rock-paper-scissors: number of rounds to play. If not provided, will be asked during setup.' } }, required: ['gameType'] @@ -147,7 +167,7 @@ export async function handleToolCall(name: string, args: any, server?: any) { if (!isSupportedGameType(genericGameType)) { throw new Error(`Unsupported game type: ${genericGameType}`) } - return await createGameWithElicitation(genericGameType, genericGameId, server) + return await createGameWithElicitation(genericGameType, genericGameId, server, args) default: throw new Error(`Unknown tool: ${name}`) @@ -160,7 +180,7 @@ export async function handleToolCall(name: string, args: any, server?: any) { /** * Create game with interactive elicitation */ -async function createGameWithElicitation(gameType: string, gameId?: string, server?: any) { +async function createGameWithElicitation(gameType: string, gameId?: string, server?: any, toolArgs?: any) { if (!server) { // Fallback to regular creation if no server for elicitation return await createGame(gameType, DEFAULT_PLAYER_NAME, gameId, DEFAULT_AI_DIFFICULTY) @@ -170,8 +190,10 @@ async function createGameWithElicitation(gameType: string, gameId?: string, serv // Elicit user preferences const elicitationResult = await elicitGameCreationPreferences(server, gameType, { gameId, - playerName: 'Player', - aiDifficulty: 'medium' + playerName: toolArgs?.playerName, + difficulty: toolArgs?.difficulty, + playerSymbol: toolArgs?.playerSymbol, + maxRounds: toolArgs?.maxRounds }) if (elicitationResult.action === 'decline' || elicitationResult.action === 'cancel') { diff --git a/mcp-server/src/integration/integration.test.ts b/mcp-server/src/integration/integration.test.ts index 78e2e7a..da77b94 100644 --- a/mcp-server/src/integration/integration.test.ts +++ b/mcp-server/src/integration/integration.test.ts @@ -88,7 +88,7 @@ describe('MCP Server Integration', () => { currentPlayerId: 'player1', players: { player1: 'Human', ai: 'AI' } }, - aiDifficulty: 'medium' + difficulty: 'medium' } ]) @@ -154,7 +154,7 @@ describe('MCP Server Integration', () => { status: 'playing', currentPlayerId: 'ai' }, - aiDifficulty: 'medium' + difficulty: 'medium' }) mockSubmitMoveViaAPI.mockResolvedValue({ diff --git a/mcp-server/src/utils/http-client.ts b/mcp-server/src/utils/http-client.ts index 0fcda26..650be33 100644 --- a/mcp-server/src/utils/http-client.ts +++ b/mcp-server/src/utils/http-client.ts @@ -27,7 +27,7 @@ export async function createGameViaAPI( gameType: string, playerName: string, gameId?: string, - aiDifficulty?: string, + difficulty?: string, gameSpecificOptions?: Record ) { try { @@ -35,8 +35,8 @@ export async function createGameViaAPI( if (gameId) { data.gameId = gameId } - if (aiDifficulty) { - data.aiDifficulty = aiDifficulty + if (difficulty) { + data.difficulty = difficulty } // Merge any game-specific options if (gameSpecificOptions) { diff --git a/shared/src/index.ts b/shared/src/index.ts index 6c41d70..e8873f9 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -3,5 +3,4 @@ export type * from './types/games'; export * from './games/index'; export * from './utils/index'; export * from './storage/index'; -export * from './testing/index'; export * from './constants/index'; diff --git a/shared/src/testing/api-test-utils.ts b/shared/src/testing/api-test-utils.ts index 11035d3..8498b6f 100644 --- a/shared/src/testing/api-test-utils.ts +++ b/shared/src/testing/api-test-utils.ts @@ -87,7 +87,7 @@ export function createMockGameSession(gameState: T, gam gameState, gameType, history: [], - aiDifficulty: 'medium' + difficulty: 'medium' } } diff --git a/shared/src/types/game.ts b/shared/src/types/game.ts index 6d448d6..aa837d2 100644 --- a/shared/src/types/game.ts +++ b/shared/src/types/game.ts @@ -44,5 +44,5 @@ export interface GameSession { gameState: TGameState; gameType: GameType; history: GameMove[]; - aiDifficulty?: Difficulty; + difficulty?: Difficulty; } diff --git a/web/src/app/api/games/rock-paper-scissors/route.ts b/web/src/app/api/games/rock-paper-scissors/route.ts index 8693798..3e2cabb 100644 --- a/web/src/app/api/games/rock-paper-scissors/route.ts +++ b/web/src/app/api/games/rock-paper-scissors/route.ts @@ -8,7 +8,7 @@ const rpsGame = new RockPaperScissorsGame() export async function POST(request: NextRequest) { try { - const { playerName, gameId, aiDifficulty, maxRounds } = await request.json() + const { playerName, gameId, difficulty, maxRounds } = await request.json() const players: Player[] = [ { id: 'player1', name: playerName || 'Player', isAI: false }, @@ -26,7 +26,7 @@ export async function POST(request: NextRequest) { gameState, gameType: 'rock-paper-scissors', history: [], - aiDifficulty: aiDifficulty || 'medium' + difficulty: difficulty || 'medium' } await setRPSGame(gameState.id, gameSession) diff --git a/web/src/app/api/games/tic-tac-toe/route.ts b/web/src/app/api/games/tic-tac-toe/route.ts index 137fd3d..ae2c87c 100644 --- a/web/src/app/api/games/tic-tac-toe/route.ts +++ b/web/src/app/api/games/tic-tac-toe/route.ts @@ -8,7 +8,7 @@ const ticTacToeGame = new TicTacToeGame() export async function POST(request: NextRequest) { try { - const { playerName, gameId, aiDifficulty, playerSymbol } = await request.json() + const { playerName, gameId, difficulty, playerSymbol } = await request.json() const players: Player[] = [ { id: 'player1', name: playerName || 'Player', isAI: false }, @@ -37,7 +37,7 @@ export async function POST(request: NextRequest) { gameState, gameType: 'tic-tac-toe', history: [], - aiDifficulty: aiDifficulty || 'medium' + difficulty: difficulty || 'medium' } await setTicTacToeGame(gameState.id, gameSession) diff --git a/web/src/app/games/rock-paper-scissors/page.tsx b/web/src/app/games/rock-paper-scissors/page.tsx index a948a54..14739e2 100644 --- a/web/src/app/games/rock-paper-scissors/page.tsx +++ b/web/src/app/games/rock-paper-scissors/page.tsx @@ -80,9 +80,9 @@ export default function RockPaperScissorsPage() { setError(null) try { - const body: { playerName: string; gameId?: string; aiDifficulty: string; maxRounds: number } = { + const body: { playerName: string; gameId?: string; difficulty: string; maxRounds: number } = { playerName: 'Player', - aiDifficulty, + difficulty: aiDifficulty, maxRounds } if (customGameId) { @@ -239,7 +239,7 @@ export default function RockPaperScissorsPage() { <>
@@ -344,7 +344,7 @@ export default function RockPaperScissorsPage() {
Round: {game.gameState.currentRound + 1}/{game.gameState.maxRounds} • Turn: {game.gameState.currentPlayerId === 'player1' ? 'Player' : 'AI'} • - Difficulty: {game.aiDifficulty || 'medium'} + Difficulty: {game.difficulty || 'medium'}
diff --git a/web/src/app/games/tic-tac-toe/page.tsx b/web/src/app/games/tic-tac-toe/page.tsx index c3d87df..2f4a754 100644 --- a/web/src/app/games/tic-tac-toe/page.tsx +++ b/web/src/app/games/tic-tac-toe/page.tsx @@ -80,9 +80,9 @@ export default function TicTacToePage() { setError(null) try { - const body: { playerName: string; gameId?: string; aiDifficulty: string; playerSymbol: string } = { + const body: { playerName: string; gameId?: string; difficulty: string; playerSymbol: string } = { playerName: 'Player', - aiDifficulty, + difficulty: aiDifficulty, playerSymbol } if (customGameId) { @@ -239,7 +239,7 @@ export default function TicTacToePage() { <> - {game.aiDifficulty || 'medium'} + {game.difficulty || 'medium'} From 6af99ca5b9c261b09e7768b732cac94fae81d966 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:30:42 +0100 Subject: [PATCH 20/37] Add elicitation tests --- .../src/handlers/elicitation-handlers.test.ts | 614 ++++++++++++++++++ mcp-server/vitest.setup.ts | 49 +- .../[id]/move/route.test.ts | 30 +- .../games/rock-paper-scissors/route.test.ts | 15 +- .../games/tic-tac-toe/[id]/move/route.test.ts | 26 +- .../app/api/games/tic-tac-toe/route.test.ts | 9 - web/vitest.setup.ts | 26 +- 7 files changed, 713 insertions(+), 56 deletions(-) create mode 100644 mcp-server/src/handlers/elicitation-handlers.test.ts diff --git a/mcp-server/src/handlers/elicitation-handlers.test.ts b/mcp-server/src/handlers/elicitation-handlers.test.ts new file mode 100644 index 0000000..e6fe9dc --- /dev/null +++ b/mcp-server/src/handlers/elicitation-handlers.test.ts @@ -0,0 +1,614 @@ +/** + * Tests for elicitation handlers + * + * These tests ensure that elicitation is properly triggered when game creation + * parameters are missing, and skipped when all required details are provided. + */ + +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' +import { + elicitGameCreationPreferences, + elicitMidGameDecision, + elicitGameCompletionFeedback, + elicitStrategyPreference, + elicitErrorRecovery, + type ElicitationResult +} from './elicitation-handlers' +import { DIFFICULTIES, DEFAULT_PLAYER_NAME, DEFAULT_AI_DIFFICULTY, GAME_TYPES } from '@turn-based-mcp/shared' + +// Mock server for elicitInput calls +const mockServer = { + elicitInput: vi.fn() +} + +describe('Elicitation Handlers', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + describe('elicitGameCreationPreferences', () => { + describe('Tic-Tac-Toe Game', () => { + const gameType = 'tic-tac-toe' + + it('should skip elicitation when all required details are provided', async () => { + const existingArgs = { + difficulty: 'medium' as const, + playerSymbol: 'X' as const, + playerName: 'TestPlayer' + } + + const result = await elicitGameCreationPreferences(mockServer, gameType, existingArgs) + + expect(mockServer.elicitInput).not.toHaveBeenCalled() + expect(result.action).toBe('accept') + expect(result.content).toEqual(existingArgs) + }) + + it('should trigger elicitation for optional parameters when only required difficulty is provided', async () => { + const existingArgs = { + difficulty: 'hard' as const + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { playerSymbol: 'O', playerName: 'OptionalPlayer' } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, gameType, existingArgs) + + expect(mockServer.elicitInput).toHaveBeenCalledOnce() + + // Verify that the schema only includes missing optional properties + const calledWith = mockServer.elicitInput.mock.calls[0][0] + expect(calledWith.requestedSchema.properties).not.toHaveProperty('difficulty') + expect(calledWith.requestedSchema.properties).toHaveProperty('playerSymbol') + expect(calledWith.requestedSchema.properties).toHaveProperty('playerName') + expect(calledWith.requestedSchema.required).toEqual([]) // No required properties left + + // Result should merge existing args with elicitation response + expect(result.content).toEqual({ + difficulty: 'hard', + playerSymbol: 'O', + playerName: 'OptionalPlayer' + }) + }) + + it('should trigger elicitation when no details are provided', async () => { + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { difficulty: 'easy', playerSymbol: 'O', playerName: 'NewPlayer' } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, gameType) + + expect(mockServer.elicitInput).toHaveBeenCalledOnce() + expect(mockServer.elicitInput).toHaveBeenCalledWith({ + message: expect.stringContaining("Let's set up your tic tac-toe game!"), + requestedSchema: expect.objectContaining({ + type: 'object', + properties: expect.objectContaining({ + difficulty: expect.objectContaining({ + type: 'string', + enum: DIFFICULTIES + }), + playerSymbol: expect.objectContaining({ + type: 'string', + enum: ['X', 'O'] + }), + playerName: expect.objectContaining({ + type: 'string' + }) + }), + required: ['difficulty'] + }) + }) + expect(result).toEqual(elicitationResponse) + }) + + it('should trigger elicitation when some optional details are missing', async () => { + const existingArgs = { + playerName: 'PartialPlayer' + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { difficulty: 'medium' } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, gameType, existingArgs) + + expect(mockServer.elicitInput).toHaveBeenCalledOnce() + + // Verify that the schema only includes missing properties + const calledWith = mockServer.elicitInput.mock.calls[0][0] + expect(calledWith.requestedSchema.properties).not.toHaveProperty('playerName') + expect(calledWith.requestedSchema.properties).toHaveProperty('difficulty') + expect(calledWith.requestedSchema.properties).toHaveProperty('playerSymbol') + + // Result should merge existing args with elicitation response + expect(result.content).toEqual({ + playerName: 'PartialPlayer', + difficulty: 'medium' + }) + }) + + it('should handle elicitation failure gracefully', async () => { + const error = new Error('Elicitation failed') + mockServer.elicitInput.mockRejectedValue(error) + + const result = await elicitGameCreationPreferences(mockServer, gameType) + + expect(mockServer.elicitInput).toHaveBeenCalledOnce() + expect(result.action).toBe('accept') + expect(result.content).toEqual({ + difficulty: DEFAULT_AI_DIFFICULTY, + playerName: DEFAULT_PLAYER_NAME, + playerSymbol: 'X' + }) + }) + + it('should filter out empty string values from existing args', async () => { + const existingArgs = { + difficulty: '', + playerSymbol: 'X', + playerName: '' + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { difficulty: 'hard', playerName: 'FilledPlayer' } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, gameType, existingArgs) + + expect(mockServer.elicitInput).toHaveBeenCalledOnce() + + // Should elicit difficulty and playerName but not playerSymbol + const calledWith = mockServer.elicitInput.mock.calls[0][0] + expect(calledWith.requestedSchema.properties).toHaveProperty('difficulty') + expect(calledWith.requestedSchema.properties).toHaveProperty('playerName') + expect(calledWith.requestedSchema.properties).not.toHaveProperty('playerSymbol') + + expect(result.content).toEqual({ + difficulty: 'hard', + playerSymbol: 'X', + playerName: 'FilledPlayer' + }) + }) + }) + + describe('Rock-Paper-Scissors Game', () => { + const gameType = 'rock-paper-scissors' + + it('should skip elicitation when all details are provided', async () => { + const existingArgs = { + difficulty: 'hard' as const, + maxRounds: 5, + playerName: 'RPSPlayer' + } + + const result = await elicitGameCreationPreferences(mockServer, gameType, existingArgs) + + expect(mockServer.elicitInput).not.toHaveBeenCalled() + expect(result.action).toBe('accept') + expect(result.content).toEqual(existingArgs) + }) + + it('should trigger elicitation for optional parameters when only required difficulty is provided', async () => { + const existingArgs = { + difficulty: 'easy' as const + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { maxRounds: 5, playerName: 'OptionalRPSPlayer' } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, gameType, existingArgs) + + expect(mockServer.elicitInput).toHaveBeenCalledOnce() + + // Verify that the schema only includes missing optional properties + const calledWith = mockServer.elicitInput.mock.calls[0][0] + expect(calledWith.requestedSchema.properties).not.toHaveProperty('difficulty') + expect(calledWith.requestedSchema.properties).toHaveProperty('maxRounds') + expect(calledWith.requestedSchema.properties).toHaveProperty('playerName') + expect(calledWith.requestedSchema.required).toEqual([]) // No required properties left + + // Result should merge existing args with elicitation response + expect(result.content).toEqual({ + difficulty: 'easy', + maxRounds: 5, + playerName: 'OptionalRPSPlayer' + }) + }) + + it('should trigger elicitation when no details are provided', async () => { + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { difficulty: 'medium', maxRounds: 3, playerName: 'RPSNewPlayer' } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, gameType) + + expect(mockServer.elicitInput).toHaveBeenCalledOnce() + expect(mockServer.elicitInput).toHaveBeenCalledWith({ + message: expect.stringContaining("Let's set up your rock paper-scissors game!"), + requestedSchema: expect.objectContaining({ + type: 'object', + properties: expect.objectContaining({ + difficulty: expect.objectContaining({ + type: 'string', + enum: DIFFICULTIES + }), + maxRounds: expect.objectContaining({ + type: 'number', + minimum: 1, + maximum: 10 + }), + playerName: expect.objectContaining({ + type: 'string' + }) + }), + required: ['difficulty'] + }) + }) + expect(result).toEqual(elicitationResponse) + }) + + it('should trigger elicitation when some optional details are missing', async () => { + const existingArgs = { + maxRounds: 7 + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { difficulty: 'hard', playerName: 'PartialRPSPlayer' } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, gameType, existingArgs) + + expect(mockServer.elicitInput).toHaveBeenCalledOnce() + + // Verify that the schema only includes missing properties + const calledWith = mockServer.elicitInput.mock.calls[0][0] + expect(calledWith.requestedSchema.properties).not.toHaveProperty('maxRounds') + expect(calledWith.requestedSchema.properties).toHaveProperty('difficulty') + expect(calledWith.requestedSchema.properties).toHaveProperty('playerName') + + // Result should merge existing args with elicitation response + expect(result.content).toEqual({ + maxRounds: 7, + difficulty: 'hard', + playerName: 'PartialRPSPlayer' + }) + }) + + it('should handle elicitation failure gracefully', async () => { + const error = new Error('Network error') + mockServer.elicitInput.mockRejectedValue(error) + + const result = await elicitGameCreationPreferences(mockServer, gameType) + + expect(mockServer.elicitInput).toHaveBeenCalledOnce() + expect(result.action).toBe('accept') + expect(result.content).toEqual({ + difficulty: DEFAULT_AI_DIFFICULTY, + playerName: DEFAULT_PLAYER_NAME, + maxRounds: 3 + }) + }) + + it('should trigger elicitation for missing parameters when some args are null/undefined', async () => { + const existingArgs = { + difficulty: 'medium' as const, + maxRounds: null, + playerName: undefined + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { maxRounds: 4, playerName: 'FilledPlayer' } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, gameType, existingArgs) + + expect(mockServer.elicitInput).toHaveBeenCalledOnce() + + // Should elicit maxRounds and playerName since they're null/undefined + const calledWith = mockServer.elicitInput.mock.calls[0][0] + expect(calledWith.requestedSchema.properties).not.toHaveProperty('difficulty') + expect(calledWith.requestedSchema.properties).toHaveProperty('maxRounds') + expect(calledWith.requestedSchema.properties).toHaveProperty('playerName') + + expect(result.content).toEqual({ + difficulty: 'medium', + maxRounds: 4, + playerName: 'FilledPlayer' + }) + }) + }) + + describe('Invalid Game Type', () => { + it('should throw error for unsupported game type', async () => { + const invalidGameType = 'checkers' + + await expect( + elicitGameCreationPreferences(mockServer, invalidGameType) + ).rejects.toThrow('No elicitation schema defined for game type: checkers') + + expect(mockServer.elicitInput).not.toHaveBeenCalled() + }) + }) + + describe('Edge Cases', () => { + it('should handle user cancellation gracefully', async () => { + const elicitationResponse: ElicitationResult = { + action: 'cancel' + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, 'tic-tac-toe') + + expect(result.action).toBe('cancel') + expect(result.content).toBeUndefined() + }) + + it('should handle user decline gracefully', async () => { + const elicitationResponse: ElicitationResult = { + action: 'decline' + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, 'rock-paper-scissors') + + expect(result.action).toBe('decline') + expect(result.content).toBeUndefined() + }) + + it('should preserve existing args when user accepts partial input', async () => { + const existingArgs = { + playerName: 'ExistingPlayer', + difficulty: undefined // This should be elicited + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { difficulty: 'easy' } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCreationPreferences(mockServer, 'tic-tac-toe', existingArgs) + + expect(result.content).toEqual({ + playerName: 'ExistingPlayer', + difficulty: 'easy' + }) + }) + }) + }) + + describe('elicitMidGameDecision', () => { + it('should create proper schema for mid-game decisions', async () => { + const context = { + gameType: 'tic-tac-toe', + gameId: 'game123', + situation: 'The AI is about to win!', + options: [ + { value: 'continue', label: 'Keep Playing' }, + { value: 'hint', label: 'Get a Hint', description: 'Show me the best move' }, + { value: 'restart', label: 'Restart Game' } + ] + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { choice: 'hint', feedback: 'This is challenging!' } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitMidGameDecision(mockServer, context) + + expect(mockServer.elicitInput).toHaveBeenCalledWith({ + message: expect.stringContaining('tic tac-toe Game Decision'), + requestedSchema: expect.objectContaining({ + type: 'object', + properties: expect.objectContaining({ + choice: expect.objectContaining({ + type: 'string', + enum: ['continue', 'hint', 'restart'], + enumNames: ['Keep Playing', 'Get a Hint', 'Restart Game'] + }), + feedback: expect.objectContaining({ + type: 'string' + }) + }), + required: ['choice'] + }) + }) + expect(result).toEqual(elicitationResponse) + }) + + it('should handle elicitation failure with default choice', async () => { + const context = { + gameType: 'rock-paper-scissors', + gameId: 'rps456', + situation: 'Choose your strategy', + options: [ + { value: 'aggressive', label: 'Aggressive Play' }, + { value: 'defensive', label: 'Defensive Play' } + ] + } + mockServer.elicitInput.mockRejectedValue(new Error('Failed')) + + const result = await elicitMidGameDecision(mockServer, context) + + expect(result.action).toBe('accept') + expect(result.content).toEqual({ choice: 'aggressive' }) + }) + }) + + describe('elicitGameCompletionFeedback', () => { + it('should create proper schema for game completion feedback', async () => { + const context = { + gameType: 'tic-tac-toe', + gameId: 'game789', + result: 'win' as const, + difficulty: 'hard' + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { + difficultyFeedback: 'just_right', + playAgain: true, + gameTypeForNext: 'rock-paper-scissors', + comments: 'Great game!' + } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitGameCompletionFeedback(mockServer, context) + + expect(mockServer.elicitInput).toHaveBeenCalledWith({ + message: expect.stringContaining('🎉 Congratulations! You won!'), + requestedSchema: expect.objectContaining({ + type: 'object', + properties: expect.objectContaining({ + difficultyFeedback: expect.objectContaining({ + type: 'string', + enum: ['too_easy', 'just_right', 'too_hard'] + }), + playAgain: expect.objectContaining({ + type: 'boolean' + }), + gameTypeForNext: expect.objectContaining({ + type: 'string', + enum: ['same', ...GAME_TYPES] + }) + }), + required: ['difficultyFeedback', 'playAgain'] + }) + }) + expect(result).toEqual(elicitationResponse) + }) + + it('should handle different result types with correct messages', async () => { + const contexts = [ + { result: 'loss' as const, expectedMessage: '😅 Good game! The AI won this time.' }, + { result: 'draw' as const, expectedMessage: '🤝 It\'s a draw! Well played by both sides.' } + ] + + for (const { result, expectedMessage } of contexts) { + mockServer.elicitInput.mockResolvedValue({ action: 'accept', content: {} }) + + await elicitGameCompletionFeedback(mockServer, { + gameType: 'rock-paper-scissors', + gameId: 'test', + result, + difficulty: 'medium' + }) + + expect(mockServer.elicitInput).toHaveBeenCalledWith({ + message: expect.stringContaining(expectedMessage), + requestedSchema: expect.any(Object) + }) + + mockServer.elicitInput.mockClear() + } + }) + }) + + describe('elicitStrategyPreference', () => { + it('should create proper schema for strategy preferences', async () => { + const context = { + gameType: 'tic-tac-toe', + gameId: 'strategy123', + availableHints: ['basic', 'advanced'], + currentSituation: 'You have a winning move available' + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { wantHint: true, hintType: 'intermediate', explainMoves: true } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitStrategyPreference(mockServer, context) + + expect(mockServer.elicitInput).toHaveBeenCalledWith({ + message: expect.stringContaining('Strategy Assistance Available'), + requestedSchema: expect.objectContaining({ + properties: expect.objectContaining({ + wantHint: expect.objectContaining({ type: 'boolean' }), + hintType: expect.objectContaining({ + type: 'string', + enum: ['beginner', 'intermediate', 'advanced'] + }) + }), + required: ['wantHint'] + }) + }) + expect(result).toEqual(elicitationResponse) + }) + }) + + describe('elicitErrorRecovery', () => { + it('should create proper schema for error recovery', async () => { + const context = { + gameType: 'rock-paper-scissors', + gameId: 'error123', + error: 'Invalid move detected', + recoveryOptions: [ + { value: 'retry', label: 'Try Again', description: 'Attempt the move again' }, + { value: 'reset', label: 'Reset Game', description: 'Start over from the beginning' } + ] + } + const elicitationResponse: ElicitationResult = { + action: 'accept', + content: { action: 'retry', reportIssue: true } + } + mockServer.elicitInput.mockResolvedValue(elicitationResponse) + + const result = await elicitErrorRecovery(mockServer, context) + + expect(mockServer.elicitInput).toHaveBeenCalledWith({ + message: expect.stringContaining('⚠️ **rock paper-scissors Game Issue**'), + requestedSchema: expect.objectContaining({ + properties: expect.objectContaining({ + action: expect.objectContaining({ + type: 'string', + enum: ['retry', 'reset'], + enumNames: ['Try Again', 'Reset Game'] + }), + reportIssue: expect.objectContaining({ type: 'boolean' }) + }), + required: ['action'] + }) + }) + expect(result).toEqual(elicitationResponse) + }) + + it('should handle elicitation failure with default recovery option', async () => { + const context = { + gameType: 'tic-tac-toe', + gameId: 'error456', + error: 'Network timeout', + recoveryOptions: [ + { value: 'reconnect', label: 'Reconnect', description: 'Try to reconnect' } + ] + } + mockServer.elicitInput.mockRejectedValue(new Error('Failed')) + + const result = await elicitErrorRecovery(mockServer, context) + + expect(result.action).toBe('accept') + expect(result.content).toEqual({ + action: 'reconnect', + reportIssue: false + }) + }) + }) +}) diff --git a/mcp-server/vitest.setup.ts b/mcp-server/vitest.setup.ts index d2467e6..5f8758d 100644 --- a/mcp-server/vitest.setup.ts +++ b/mcp-server/vitest.setup.ts @@ -4,7 +4,54 @@ * Uses shared test setup utilities to ensure consistent database setup */ -import { setupStandardTestDatabase } from '@turn-based-mcp/shared' +import { vi } from 'vitest' +import { setupStandardTestDatabase } from '@turn-based-mcp/shared/dist/testing/vitest-setup.js' // Setup standard test database using shared utility setupStandardTestDatabase() + +// Suppress expected console errors during tests +// These are typically from error handling scenarios that we're intentionally testing +const originalConsoleError = console.error +console.error = (...args: any[]) => { + const message = args[0] + + if (typeof message === 'string') { + // Suppress expected error messages from elicitation handlers + if ( + message.includes('Elicitation failed:') || + message.includes('Mid-game elicitation failed:') || + message.includes('Error recovery elicitation failed:') || + message.includes('Completion feedback elicitation failed:') || + message.includes('Strategy elicitation failed:') + ) { + return // Suppress these expected errors + } + + // Suppress expected error messages from resource handlers + if ( + message.includes('Error listing') && message.includes('games:') || + message.includes('Error listing resources:') + ) { + return // Suppress these expected errors + } + + // Suppress expected error messages from HTTP client + if ( + message.includes('Error fetching') && message.includes('game via API:') || + message.includes('Error creating') && message.includes('game via API:') + ) { + return // Suppress these expected errors + } + + // Suppress expected server error messages (but not startup messages) + if ( + message.includes('Server error:') + ) { + return // Suppress these expected errors + } + } + + // For all other errors, use the original console.error + originalConsoleError(...args) +} diff --git a/web/src/app/api/games/rock-paper-scissors/[id]/move/route.test.ts b/web/src/app/api/games/rock-paper-scissors/[id]/move/route.test.ts index 70b38ec..8e4e90f 100644 --- a/web/src/app/api/games/rock-paper-scissors/[id]/move/route.test.ts +++ b/web/src/app/api/games/rock-paper-scissors/[id]/move/route.test.ts @@ -217,8 +217,6 @@ describe('/api/games/rock-paper-scissors/[id]/move', () => { }); it('should handle JSON parsing errors', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(); - const request = new NextRequest('http://localhost:3000/api/games/rock-paper-scissors/test-rps-1/move', { method: 'POST', body: 'invalid-json' @@ -230,36 +228,34 @@ describe('/api/games/rock-paper-scissors/[id]/move', () => { expect(response.status).toBe(500); expect(responseData).toEqual({ error: 'Failed to process move' }); - expect(consoleSpy).toHaveBeenCalledWith('Error processing move:', expect.any(Error)); - - consoleSpy.mockRestore(); }); - it('should handle storage errors', async () => { - const move: RPSMove = { choice: 'rock' }; - const storageError = new Error('Database connection failed'); - - mockGetRPSGame.mockRejectedValue(storageError); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(); + it('should handle storage errors gracefully', async () => { + mockGetRPSGame.mockResolvedValue(mockGameSession); + mockGame.validateMove.mockReturnValue(true); + mockGame.applyMove.mockReturnValue(mockGameState); + mockGame.checkGameEnd.mockReturnValue(null); + mockSetRPSGame.mockRejectedValue(new Error('Storage failed')); - const request = new NextRequest('http://localhost:3000/api/games/rock-paper-scissors/test-rps-1/move', { + const request = new NextRequest('http://localhost/api/games/rock-paper-scissors/test-game/move', { method: 'POST', - body: JSON.stringify({ move, playerId: 'player1' }) + body: JSON.stringify({ + playerId: 'player1', + move: { choice: 'rock' } + }) }); - const params = Promise.resolve({ id: 'test-rps-1' }); + const params = Promise.resolve({ id: 'test-game' }); const response = await POST(request, { params }); const responseData = await response.json(); expect(response.status).toBe(500); expect(responseData).toEqual({ error: 'Failed to process move' }); - expect(consoleSpy).toHaveBeenCalledWith('Error processing move:', storageError); - - consoleSpy.mockRestore(); }); it('should handle missing request body fields', async () => { mockGetRPSGame.mockResolvedValue(mockGameSession); + mockGame.validateMove.mockReturnValue(false); const request = new NextRequest('http://localhost:3000/api/games/rock-paper-scissors/test-rps-1/move', { method: 'POST', diff --git a/web/src/app/api/games/rock-paper-scissors/route.test.ts b/web/src/app/api/games/rock-paper-scissors/route.test.ts index 1739848..d110a89 100644 --- a/web/src/app/api/games/rock-paper-scissors/route.test.ts +++ b/web/src/app/api/games/rock-paper-scissors/route.test.ts @@ -69,7 +69,7 @@ describe('/api/games/rock-paper-scissors', () => { expect(mockGame.getInitialState).toHaveBeenCalledWith([ { id: 'player1', name: 'TestPlayer', isAI: false }, { id: 'ai', name: 'AI', isAI: true } - ]); + ], { maxRounds: undefined }); expect(mockGameStorage.setRPSGame).toHaveBeenCalledWith( mockGameState.id, expect.objectContaining({ @@ -99,7 +99,7 @@ describe('/api/games/rock-paper-scissors', () => { expect(mockGame.getInitialState).toHaveBeenCalledWith([ { id: 'player1', name: 'Player', isAI: false }, { id: 'ai', name: 'AI', isAI: true } - ]); + ], { maxRounds: undefined }); }); it('should handle empty request body', async () => { @@ -116,13 +116,12 @@ describe('/api/games/rock-paper-scissors', () => { expect(mockGame.getInitialState).toHaveBeenCalledWith([ { id: 'player1', name: 'Player', isAI: false }, { id: 'ai', name: 'AI', isAI: true } - ]); + ], { maxRounds: undefined }); }); it('should handle storage errors', async () => { const storageError = new Error('Storage failed'); mockGameStorage.setRPSGame.mockRejectedValue(storageError); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const request = new NextRequest('http://localhost:3000/api/games/rock-paper-scissors', { method: 'POST', @@ -134,14 +133,9 @@ describe('/api/games/rock-paper-scissors', () => { expect(response.status).toBe(500); expect(responseData).toEqual({ error: 'Failed to create game' }); - expect(consoleSpy).toHaveBeenCalledWith('Error creating game:', storageError); - - consoleSpy.mockRestore(); }); it('should handle JSON parsing errors', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const request = new NextRequest('http://localhost:3000/api/games/rock-paper-scissors', { method: 'POST', body: 'invalid-json' @@ -152,9 +146,6 @@ describe('/api/games/rock-paper-scissors', () => { expect(response.status).toBe(500); expect(responseData).toEqual({ error: 'Failed to create game' }); - expect(consoleSpy).toHaveBeenCalledWith('Error creating game:', expect.any(Error)); - - consoleSpy.mockRestore(); }); }); diff --git a/web/src/app/api/games/tic-tac-toe/[id]/move/route.test.ts b/web/src/app/api/games/tic-tac-toe/[id]/move/route.test.ts index 34d346a..52940d0 100644 --- a/web/src/app/api/games/tic-tac-toe/[id]/move/route.test.ts +++ b/web/src/app/api/games/tic-tac-toe/[id]/move/route.test.ts @@ -216,8 +216,6 @@ describe('/api/games/tic-tac-toe/[id]/move', () => { }); it('should handle JSON parsing errors', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(); - const request = new NextRequest('http://localhost:3000/api/games/tic-tac-toe/test-game-1/move', { method: 'POST', body: 'invalid-json' @@ -229,21 +227,21 @@ describe('/api/games/tic-tac-toe/[id]/move', () => { expect(response.status).toBe(500); expect(responseData).toEqual({ error: 'Failed to process move' }); - expect(consoleSpy).toHaveBeenCalledWith('Error processing move:', expect.any(Error)); - - consoleSpy.mockRestore(); }); - it('should handle storage errors', async () => { - const move: TicTacToeMove = { row: 0, col: 0 }; - const storageError = new Error('Database connection failed'); - - mockGetTicTacToeGame.mockRejectedValue(storageError); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(); + it('should handle storage errors gracefully', async () => { + mockGetTicTacToeGame.mockResolvedValue(mockGameSession); + mockGame.validateMove.mockReturnValue(true); + mockGame.applyMove.mockReturnValue(mockGameState); + mockGame.checkGameEnd.mockReturnValue(null); + mockSetTicTacToeGame.mockRejectedValue(new Error('Storage failed')); const request = new NextRequest('http://localhost:3000/api/games/tic-tac-toe/test-game-1/move', { method: 'POST', - body: JSON.stringify({ move, playerId: 'player1' }) + body: JSON.stringify({ + playerId: 'player1', + move: { row: 0, col: 0 } + }) }); const params = Promise.resolve({ id: 'test-game-1' }); @@ -252,13 +250,11 @@ describe('/api/games/tic-tac-toe/[id]/move', () => { expect(response.status).toBe(500); expect(responseData).toEqual({ error: 'Failed to process move' }); - expect(consoleSpy).toHaveBeenCalledWith('Error processing move:', storageError); - - consoleSpy.mockRestore(); }); it('should handle missing request body fields', async () => { mockGetTicTacToeGame.mockResolvedValue(mockGameSession); + mockGame.validateMove.mockReturnValue(false); const request = new NextRequest('http://localhost:3000/api/games/tic-tac-toe/test-game-1/move', { method: 'POST', diff --git a/web/src/app/api/games/tic-tac-toe/route.test.ts b/web/src/app/api/games/tic-tac-toe/route.test.ts index 93b8e5f..d874abe 100644 --- a/web/src/app/api/games/tic-tac-toe/route.test.ts +++ b/web/src/app/api/games/tic-tac-toe/route.test.ts @@ -133,7 +133,6 @@ describe('/api/games/tic-tac-toe', () => { it('should handle storage errors', async () => { const storageError = new Error('Storage failed'); mockGameStorage.setTicTacToeGame.mockRejectedValue(storageError); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const request = new NextRequest('http://localhost:3000/api/games/tic-tac-toe', { method: 'POST', @@ -145,14 +144,9 @@ describe('/api/games/tic-tac-toe', () => { expect(response.status).toBe(500); expect(responseData).toEqual({ error: 'Failed to create game' }); - expect(consoleSpy).toHaveBeenCalledWith('Error creating game:', storageError); - - consoleSpy.mockRestore(); }); it('should handle JSON parsing errors', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const request = new NextRequest('http://localhost:3000/api/games/tic-tac-toe', { method: 'POST', body: 'invalid-json' @@ -163,9 +157,6 @@ describe('/api/games/tic-tac-toe', () => { expect(response.status).toBe(500); expect(responseData).toEqual({ error: 'Failed to create game' }); - expect(consoleSpy).toHaveBeenCalledWith('Error creating game:', expect.any(Error)); - - consoleSpy.mockRestore(); }); }); diff --git a/web/vitest.setup.ts b/web/vitest.setup.ts index 4dac7fb..c6e3f02 100644 --- a/web/vitest.setup.ts +++ b/web/vitest.setup.ts @@ -3,7 +3,7 @@ import { vi } from 'vitest' import '@testing-library/jest-dom' // Use shared test database setup -import { setupStandardTestDatabase } from '@turn-based-mcp/shared' +import { setupStandardTestDatabase } from '@turn-based-mcp/shared/dist/testing/vitest-setup.js' // Setup standard test database using shared utility setupStandardTestDatabase() @@ -65,6 +65,28 @@ vi.mock('next/server', () => ({ // Mock fetch globally global.fetch = vi.fn() +// Suppress expected console errors during tests +// These are typically from error handling scenarios that we're intentionally testing +const originalConsoleError = console.error +const suppressedConsoleError = (...args: any[]) => { + const message = args[0] + + if (typeof message === 'string') { + // Suppress expected error messages from API routes + if ( + message.includes('Error creating game:') || + message.includes('Error deleting game:') || + message.includes('Error processing move:') || + message.includes('Error fetching games for MCP:') + ) { + return // Suppress these expected errors + } + } + + // For all other errors, use the original console.error + originalConsoleError(...args) +} + // Mock console methods to reduce noise during tests global.console = { ...console, @@ -72,5 +94,5 @@ global.console = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), - error: vi.fn(), + error: suppressedConsoleError, } From b1c5231f2cb11f6706cceef1400af18c3955ded4 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:41:35 +0100 Subject: [PATCH 21/37] Fix ESLint warnings --- .vscode/mcp.json | 11 - package-lock.json | 548 +++++++++++++++++- shared/package.json | 1 + shared/src/index.ts | 1 + .../[id]/move/route.test.ts | 8 +- .../rock-paper-scissors/mcp/route.test.ts | 4 +- .../games/rock-paper-scissors/route.test.ts | 3 +- .../games/tic-tac-toe/[id]/move/route.test.ts | 8 +- .../app/api/games/tic-tac-toe/route.test.ts | 3 +- .../components/games/GameInfoPanel.test.tsx | 1 - web/src/components/games/GameStatus.test.tsx | 1 - .../shared/DifficultyBadge.test.tsx | 6 +- .../shared/MCPAssistantPanel.test.tsx | 1 - web/src/components/ui/LoadingSpinner.test.tsx | 1 - web/src/types/vitest.d.ts | 1 + web/tsconfig.json | 2 + 16 files changed, 567 insertions(+), 33 deletions(-) create mode 100644 web/src/types/vitest.d.ts diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 1ea4125..3f00ddb 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -10,17 +10,6 @@ "args": [ "@playwright/mcp@latest" ] - }, - "context7": { - "type": "http", - "url": "https://mcp.context7.com/mcp" - }, - "sequentialthinking": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-sequential-thinking" - ] } } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0422182..9cf626d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -419,6 +419,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -1668,6 +1678,24 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1681,6 +1709,16 @@ "node": ">=18.0.0" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/diff-sequences": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", @@ -2053,6 +2091,17 @@ "node": ">=10" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -3661,6 +3710,40 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -4148,6 +4231,25 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.4.tgz", + "integrity": "sha512-cxrAnZNLBnQwBPByK4CeDaw5sWZtMilJE/Q3iDA0aamgaIVNDF9T6K2/8DfYDZEejZ2jNnDrG9m8MY72HFd0KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.29", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -5038,6 +5140,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6148,6 +6257,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -6418,6 +6544,27 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6431,6 +6578,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -6599,6 +6772,13 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -6991,8 +7171,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -7260,6 +7440,60 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -7278,6 +7512,22 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-diff": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", @@ -8050,6 +8300,34 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-fetch-happen": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", @@ -8942,6 +9220,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9013,6 +9298,30 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -9981,6 +10290,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -10244,6 +10566,60 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -10357,6 +10733,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -10556,6 +10975,47 @@ "node": ">=8" } }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -11499,6 +11959,91 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -11591,6 +12136,7 @@ "devDependencies": { "@jest/types": "30.0.5", "@types/jest": "^30.0.0", + "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "fix-esm-import-path": "^1.10.3", "typescript": "^5.5.0", diff --git a/shared/package.json b/shared/package.json index 515c1de..884bd44 100644 --- a/shared/package.json +++ b/shared/package.json @@ -18,6 +18,7 @@ "devDependencies": { "@jest/types": "30.0.5", "@types/jest": "^30.0.0", + "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "fix-esm-import-path": "^1.10.3", "typescript": "^5.5.0", diff --git a/shared/src/index.ts b/shared/src/index.ts index e8873f9..1f6160a 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -4,3 +4,4 @@ export * from './games/index'; export * from './utils/index'; export * from './storage/index'; export * from './constants/index'; +export * from './testing/index'; diff --git a/web/src/app/api/games/rock-paper-scissors/[id]/move/route.test.ts b/web/src/app/api/games/rock-paper-scissors/[id]/move/route.test.ts index 8e4e90f..90819a8 100644 --- a/web/src/app/api/games/rock-paper-scissors/[id]/move/route.test.ts +++ b/web/src/app/api/games/rock-paper-scissors/[id]/move/route.test.ts @@ -1,4 +1,4 @@ -import { vi } from 'vitest' +import { vi, type MockedFunction, type MockedClass } from 'vitest' import { NextRequest } from 'next/server'; import type { GameSession, RPSGameState, RPSMove } from '@turn-based-mcp/shared'; @@ -28,11 +28,11 @@ import { POST } from './route'; import { RockPaperScissorsGame } from '@turn-based-mcp/shared'; import { getRPSGame, setRPSGame } from '../../../../../../lib/game-storage'; -const mockGetRPSGame = getRPSGame as vi.MockedFunction; -const mockSetRPSGame = setRPSGame as vi.MockedFunction; +const mockGetRPSGame = getRPSGame as MockedFunction; +const mockSetRPSGame = setRPSGame as MockedFunction; // Get access to the mock game instance from the mocked module -const mockGame = (RockPaperScissorsGame as vi.MockedClass).mock.results[0]?.value || { +const mockGame = (RockPaperScissorsGame as MockedClass).mock.results[0]?.value || { getInitialState: vi.fn(), validateMove: vi.fn(), applyMove: vi.fn(), diff --git a/web/src/app/api/games/rock-paper-scissors/mcp/route.test.ts b/web/src/app/api/games/rock-paper-scissors/mcp/route.test.ts index db7a5c2..7c19984 100644 --- a/web/src/app/api/games/rock-paper-scissors/mcp/route.test.ts +++ b/web/src/app/api/games/rock-paper-scissors/mcp/route.test.ts @@ -1,4 +1,4 @@ -import { vi } from 'vitest' +import { vi, type MockedFunction } from 'vitest' import { GET } from './route' import { getAllRPSGames } from '../../../../../lib/game-storage' import type { GameSession } from '@turn-based-mcp/shared' @@ -7,7 +7,7 @@ import type { RPSGameState } from '@turn-based-mcp/shared' // Mock the game storage vi.mock('../../../../../lib/game-storage') -const mockGetAllRPSGames = getAllRPSGames as vi.MockedFunction +const mockGetAllRPSGames = getAllRPSGames as MockedFunction describe('/api/games/rock-paper-scissors/mcp', () => { beforeEach(() => { diff --git a/web/src/app/api/games/rock-paper-scissors/route.test.ts b/web/src/app/api/games/rock-paper-scissors/route.test.ts index d110a89..c0a1151 100644 --- a/web/src/app/api/games/rock-paper-scissors/route.test.ts +++ b/web/src/app/api/games/rock-paper-scissors/route.test.ts @@ -24,8 +24,7 @@ vi.mock('../../../../lib/game-storage', () => ({ getAllRPSGames: vi.fn() })); -// Import the mocked classes -import { RockPaperScissorsGame } from '@turn-based-mcp/shared'; +// Import the mocked storage import * as gameStorage from '../../../../lib/game-storage'; // Now import the route AFTER the mocks are set up diff --git a/web/src/app/api/games/tic-tac-toe/[id]/move/route.test.ts b/web/src/app/api/games/tic-tac-toe/[id]/move/route.test.ts index 52940d0..b3e0327 100644 --- a/web/src/app/api/games/tic-tac-toe/[id]/move/route.test.ts +++ b/web/src/app/api/games/tic-tac-toe/[id]/move/route.test.ts @@ -1,4 +1,4 @@ -import { vi } from 'vitest' +import { vi, type MockedFunction, type MockedClass } from 'vitest' import { NextRequest } from 'next/server'; import type { GameSession, TicTacToeGameState, TicTacToeMove } from '@turn-based-mcp/shared'; @@ -24,11 +24,11 @@ vi.mock('@turn-based-mcp/shared', () => { import { POST } from './route'; import { TicTacToeGame, getTicTacToeGame, setTicTacToeGame } from '@turn-based-mcp/shared'; -const mockGetTicTacToeGame = getTicTacToeGame as vi.MockedFunction; -const mockSetTicTacToeGame = setTicTacToeGame as vi.MockedFunction; +const mockGetTicTacToeGame = getTicTacToeGame as MockedFunction; +const mockSetTicTacToeGame = setTicTacToeGame as MockedFunction; // Get access to the mock game instance from the mocked module -const mockGame = (TicTacToeGame as vi.MockedClass).mock.results[0]?.value || { +const mockGame = (TicTacToeGame as MockedClass).mock.results[0]?.value || { getInitialState: vi.fn(), validateMove: vi.fn(), applyMove: vi.fn(), diff --git a/web/src/app/api/games/tic-tac-toe/route.test.ts b/web/src/app/api/games/tic-tac-toe/route.test.ts index d874abe..b28bd43 100644 --- a/web/src/app/api/games/tic-tac-toe/route.test.ts +++ b/web/src/app/api/games/tic-tac-toe/route.test.ts @@ -24,8 +24,7 @@ vi.mock('../../../../lib/game-storage', () => ({ getAllTicTacToeGames: vi.fn() })); -// Import the mocked classes -import { TicTacToeGame } from '@turn-based-mcp/shared'; +// Import the mocked storage import * as gameStorage from '../../../../lib/game-storage'; // Now import the route AFTER the mocks are set up diff --git a/web/src/components/games/GameInfoPanel.test.tsx b/web/src/components/games/GameInfoPanel.test.tsx index c6543a2..dd998d8 100644 --- a/web/src/components/games/GameInfoPanel.test.tsx +++ b/web/src/components/games/GameInfoPanel.test.tsx @@ -1,4 +1,3 @@ -import { vi } from 'vitest' import { render, screen } from '@testing-library/react' import { GameInfoPanel } from './GameInfoPanel' import type { BaseGameState } from '@turn-based-mcp/shared' diff --git a/web/src/components/games/GameStatus.test.tsx b/web/src/components/games/GameStatus.test.tsx index 2742048..677f7b4 100644 --- a/web/src/components/games/GameStatus.test.tsx +++ b/web/src/components/games/GameStatus.test.tsx @@ -1,4 +1,3 @@ -import { vi } from 'vitest' import { render, screen } from '@testing-library/react'; import { GameStatus } from './GameStatus'; import type { BaseGameState, PlayerId } from '@turn-based-mcp/shared'; diff --git a/web/src/components/shared/DifficultyBadge.test.tsx b/web/src/components/shared/DifficultyBadge.test.tsx index cf8488b..2e00734 100644 --- a/web/src/components/shared/DifficultyBadge.test.tsx +++ b/web/src/components/shared/DifficultyBadge.test.tsx @@ -1,16 +1,16 @@ -import { vi } from 'vitest' /** * Tests for DifficultyBadge component */ +import { vi } from 'vitest' import React from 'react' import { render, screen } from '@testing-library/react' import { DifficultyBadge } from './DifficultyBadge' // Mock the shared library constants vi.mock('@turn-based-mcp/shared/dist/constants/game-constants', () => ({ - getDifficultyDisplay: vi.fn((difficulty) => { - const displays = { + getDifficultyDisplay: vi.fn((difficulty: string) => { + const displays: Record = { easy: { emoji: '😌', label: 'Easy' }, medium: { emoji: '🎯', label: 'Medium' }, hard: { emoji: '🔥', label: 'Hard' } diff --git a/web/src/components/shared/MCPAssistantPanel.test.tsx b/web/src/components/shared/MCPAssistantPanel.test.tsx index b9d0cd9..fea1dc7 100644 --- a/web/src/components/shared/MCPAssistantPanel.test.tsx +++ b/web/src/components/shared/MCPAssistantPanel.test.tsx @@ -1,4 +1,3 @@ -import { vi } from 'vitest' import { render, screen } from '@testing-library/react' import { MCPAssistantPanel } from './MCPAssistantPanel' import type { BaseGameState } from '@turn-based-mcp/shared' diff --git a/web/src/components/ui/LoadingSpinner.test.tsx b/web/src/components/ui/LoadingSpinner.test.tsx index 5fac30a..7c9b794 100644 --- a/web/src/components/ui/LoadingSpinner.test.tsx +++ b/web/src/components/ui/LoadingSpinner.test.tsx @@ -1,4 +1,3 @@ -import { vi } from 'vitest' import { render, screen } from '@testing-library/react' import { LoadingSpinner } from '../ui/LoadingSpinner' diff --git a/web/src/types/vitest.d.ts b/web/src/types/vitest.d.ts new file mode 100644 index 0000000..9896c47 --- /dev/null +++ b/web/src/types/vitest.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/tsconfig.json b/web/tsconfig.json index afb1b75..72739fd 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -13,6 +13,7 @@ "isolatedModules": true, "jsx": "preserve", "incremental": true, + "types": ["vitest/globals"], "plugins": [ { "name": "next" @@ -25,6 +26,7 @@ }, "include": [ "next-env.d.ts", + "src/types/vitest.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts" From 15f7c6a5bac58bdfa486d483f19eb1817b6b75f2 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:12:42 +0100 Subject: [PATCH 22/37] Add eslint config across the board --- eslint.config.js | 31 +++++++ mcp-server/eslint.config.js | 41 ++++++++ mcp-server/package.json | 6 +- package-lock.json | 180 ++++++++++++++++++++++++------------ package.json | 9 +- shared/eslint.config.js | 41 ++++++++ shared/package.json | 7 +- web/.eslintrc.json | 6 -- web/eslint.config.js | 62 +++++++++++++ web/package.json | 7 +- 10 files changed, 319 insertions(+), 71 deletions(-) create mode 100644 eslint.config.js create mode 100644 mcp-server/eslint.config.js create mode 100644 shared/eslint.config.js delete mode 100644 web/.eslintrc.json create mode 100644 web/eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9dd4ba2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{js,mjs,cjs,ts}'], + languageOptions: { + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + }, + rules: { + // Add any global rules here + }, + }, + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/.next/**', + '**/coverage/**', + '**/*.config.js', + '**/*.config.ts', + ], + } +); diff --git a/mcp-server/eslint.config.js b/mcp-server/eslint.config.js new file mode 100644 index 0000000..24035a1 --- /dev/null +++ b/mcp-server/eslint.config.js @@ -0,0 +1,41 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{js,mjs,cjs,ts}'], + languageOptions: { + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + }, + rules: { + // MCP server specific rules + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/explicit-function-return-type': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + 'no-console': 'off', // Console logging is often needed for MCP servers + }, + }, + { + files: ['**/*.test.ts', 'vitest.*.ts'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, + }, + { + ignores: [ + 'node_modules/**', + 'dist/**', + 'coverage/**', + '*.config.js', + '*.config.ts', + ], + } +); diff --git a/mcp-server/package.json b/mcp-server/package.json index 8b876f8..275a738 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -13,16 +13,20 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "test:ui": "vitest --ui" + "test:ui": "vitest --ui", + "lint": "eslint .", + "lint:fix": "eslint . --fix" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", "@turn-based-mcp/shared": "file:../shared" }, "devDependencies": { + "@eslint/js": "^9.32.0", "@types/node": "^24.1.0", "@vitest/ui": "^3.2.4", "typescript": "^5.5.0", + "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" } } diff --git a/package-lock.json b/package-lock.json index 9cf626d..a74db0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,16 @@ "mcp-server" ], "devDependencies": { + "@eslint/js": "^9.32.0", "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/eslint__js": "^8.42.3", "@types/node": "^24.1.0", "@vitest/ui": "^3.2.4", + "eslint": "^9.32.0", "typescript": "^5.5.0", + "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" }, "engines": { @@ -33,9 +37,11 @@ "@turn-based-mcp/shared": "file:../shared" }, "devDependencies": { + "@eslint/js": "^9.32.0", "@types/node": "^24.1.0", "@vitest/ui": "^3.2.4", "typescript": "^5.5.0", + "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" } }, @@ -2974,6 +2980,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3133,17 +3160,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", - "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/type-utils": "8.38.0", - "@typescript-eslint/utils": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -3157,9 +3184,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.38.0", + "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -3173,16 +3200,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", - "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { @@ -3194,18 +3221,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", - "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.38.0", - "@typescript-eslint/types": "^8.38.0", + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "engines": { @@ -3216,18 +3243,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", - "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3238,9 +3265,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", - "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "dev": true, "license": "MIT", "engines": { @@ -3251,19 +3278,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", - "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -3276,13 +3303,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", - "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "dev": true, "license": "MIT", "engines": { @@ -3294,16 +3321,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", - "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.38.0", - "@typescript-eslint/tsconfig-utils": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3319,7 +3346,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -3379,16 +3406,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", - "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0" + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3399,17 +3426,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", - "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -11346,6 +11373,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.0.tgz", + "integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -12134,12 +12185,15 @@ "sqlite3": "^5.1.7" }, "devDependencies": { + "@eslint/js": "^9.32.0", "@jest/types": "30.0.5", "@types/jest": "^30.0.0", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", + "eslint": "^9.32.0", "fix-esm-import-path": "^1.10.3", "typescript": "^5.5.0", + "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" } }, @@ -12157,6 +12211,7 @@ "tailwind-merge": "^3.3.1" }, "devDependencies": { + "@eslint/js": "^9.32.0", "@tailwindcss/postcss": "^4.1.0", "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", @@ -12168,11 +12223,14 @@ "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", "eslint-config-next": "^15.4.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", "jsdom": "^26.1.0", "node-fetch": "^3.3.2", "postcss": "^8.4.40", "tailwindcss": "^4.1.11", "typescript": "^5.5.0", + "typescript-eslint": "^8.39.0", "undici": "^7.12.0", "vitest": "^3.2.4" } diff --git a/package.json b/package.json index 37c85af..74400c5 100644 --- a/package.json +++ b/package.json @@ -17,15 +17,22 @@ "clean": "npm run clean --workspace=shared && npm run clean --workspace=web && npm run clean --workspace=mcp-server", "test": "npm run test --workspace=shared && npm run test --workspace=web && npm run test --workspace=mcp-server", "test:watch": "npm run test:watch --workspace=shared", - "test:coverage": "npm run test:coverage --workspace=shared && npm run test:coverage --workspace=web && npm run test:coverage --workspace=mcp-server" + "test:coverage": "npm run test:coverage --workspace=shared && npm run test:coverage --workspace=web && npm run test:coverage --workspace=mcp-server", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "lint:all": "npm run lint --workspace=shared && npm run lint --workspace=web && npm run lint --workspace=mcp-server" }, "devDependencies": { + "@eslint/js": "^9.32.0", "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/eslint__js": "^8.42.3", "@types/node": "^24.1.0", "@vitest/ui": "^3.2.4", + "eslint": "^9.32.0", "typescript": "^5.5.0", + "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" }, "engines": { diff --git a/shared/eslint.config.js b/shared/eslint.config.js new file mode 100644 index 0000000..3dea93f --- /dev/null +++ b/shared/eslint.config.js @@ -0,0 +1,41 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{js,mjs,cjs,ts}'], + languageOptions: { + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + }, + rules: { + // Shared library specific rules + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/explicit-function-return-type': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + }, + }, + { + files: ['**/*.test.ts', '**/*.test.tsx', 'vitest.*.ts'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, + }, + { + ignores: [ + 'node_modules/**', + 'dist/**', + 'coverage/**', + '*.config.js', + '*.config.ts', + 'games.db', + ], + } +); diff --git a/shared/package.json b/shared/package.json index 884bd44..818ec95 100644 --- a/shared/package.json +++ b/shared/package.json @@ -13,15 +13,20 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "test:ui": "vitest --ui" + "test:ui": "vitest --ui", + "lint": "eslint .", + "lint:fix": "eslint . --fix" }, "devDependencies": { + "@eslint/js": "^9.32.0", "@jest/types": "30.0.5", "@types/jest": "^30.0.0", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", + "eslint": "^9.32.0", "fix-esm-import-path": "^1.10.3", "typescript": "^5.5.0", + "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" }, "dependencies": { diff --git a/web/.eslintrc.json b/web/.eslintrc.json deleted file mode 100644 index 6b10a5b..0000000 --- a/web/.eslintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": [ - "next/core-web-vitals", - "next/typescript" - ] -} diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..efbb807 --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,62 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import reactPlugin from 'eslint-plugin-react'; +import reactHooksPlugin from 'eslint-plugin-react-hooks'; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{js,mjs,cjs,ts,tsx}'], + plugins: { + react: reactPlugin, + 'react-hooks': reactHooksPlugin, + }, + languageOptions: { + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + // React specific rules + ...reactPlugin.configs.recommended.rules, + ...reactHooksPlugin.configs.recommended.rules, + + // TypeScript rules + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'warn', + + // Next.js specific rules + 'react/react-in-jsx-scope': 'off', // Not needed in Next.js 13+ + 'react/prop-types': 'off', // Using TypeScript for prop validation + }, + }, + { + files: ['**/*.test.{ts,tsx}', 'vitest.*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + 'react/display-name': 'off', + }, + }, + { + ignores: [ + 'node_modules/**', + '.next/**', + 'coverage/**', + '*.config.js', + '*.config.ts', + 'games.db', + ], + } +); diff --git a/web/package.json b/web/package.json index 2ffa1e7..c57d0ad 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "type-check": "tsc --noEmit", "clean": "rm -rf .next", "test": "vitest run", @@ -25,6 +26,7 @@ "tailwind-merge": "^3.3.1" }, "devDependencies": { + "@eslint/js": "^9.32.0", "@tailwindcss/postcss": "^4.1.0", "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", @@ -36,11 +38,14 @@ "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", "eslint-config-next": "^15.4.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", "jsdom": "^26.1.0", "node-fetch": "^3.3.2", "postcss": "^8.4.40", "tailwindcss": "^4.1.11", "typescript": "^5.5.0", + "typescript-eslint": "^8.39.0", "undici": "^7.12.0", "vitest": "^3.2.4" } From 963acfcdf435afe97ce21210e381439435b3d6cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:19:48 +0000 Subject: [PATCH 23/37] Initial plan From e3b001451c0124b8fa5a0232a09c307df9dc1e19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:32:11 +0000 Subject: [PATCH 24/37] Fix all ESLint errors and missing return types in shared workspace Co-authored-by: chrisreddington <791642+chrisreddington@users.noreply.github.com> --- shared/src/constants/game-constants.ts | 2 +- shared/src/games/rock-paper-scissors.ts | 2 +- shared/src/storage/game-storage.test.ts | 2 +- shared/src/storage/sqlite-storage.ts | 1 - shared/src/testing/api-test-utils.ts | 15 ++++++++++++--- shared/src/testing/test-database.ts | 2 +- shared/src/testing/vitest-setup.ts | 2 +- shared/src/types/games.ts | 2 +- 8 files changed, 18 insertions(+), 10 deletions(-) diff --git a/shared/src/constants/game-constants.ts b/shared/src/constants/game-constants.ts index 25908f8..484c315 100644 --- a/shared/src/constants/game-constants.ts +++ b/shared/src/constants/game-constants.ts @@ -81,6 +81,6 @@ export function isValidGameStatus(status: string): status is GameStatus { /** * Get difficulty display configuration */ -export function getDifficultyDisplay(difficulty: Difficulty) { +export function getDifficultyDisplay(difficulty: Difficulty): { emoji: string; label: string } { return DIFFICULTY_DISPLAY[difficulty] } \ No newline at end of file diff --git a/shared/src/games/rock-paper-scissors.ts b/shared/src/games/rock-paper-scissors.ts index c0d779c..384981f 100644 --- a/shared/src/games/rock-paper-scissors.ts +++ b/shared/src/games/rock-paper-scissors.ts @@ -95,7 +95,7 @@ export class RockPaperScissorsGame implements Game { newRounds[gameState.currentRound] = currentRound; let newCurrentRound = gameState.currentRound; - let newScores = { ...gameState.scores }; + const newScores = { ...gameState.scores }; let newCurrentPlayerId = gameState.currentPlayerId; // If both players have made their choices, resolve the round diff --git a/shared/src/storage/game-storage.test.ts b/shared/src/storage/game-storage.test.ts index b334758..a438172 100644 --- a/shared/src/storage/game-storage.test.ts +++ b/shared/src/storage/game-storage.test.ts @@ -1,7 +1,7 @@ import { vi } from 'vitest' import * as gameStorage from './game-storage'; import * as sqliteStorage from './sqlite-storage'; -import type { GameSession, Player } from '../types/game'; +import type { GameSession } from '../types/game'; import type { TicTacToeGameState, RPSGameState } from '../types/games'; // Mock sqlite-storage module diff --git a/shared/src/storage/sqlite-storage.ts b/shared/src/storage/sqlite-storage.ts index 83983e3..d2a0a1f 100644 --- a/shared/src/storage/sqlite-storage.ts +++ b/shared/src/storage/sqlite-storage.ts @@ -1,5 +1,4 @@ import sqlite3 from 'sqlite3' -import { promisify } from 'util' import path from 'path' import type { GameSession } from '../types/game' import type { TicTacToeGameState, RPSGameState } from '../types/games' diff --git a/shared/src/testing/api-test-utils.ts b/shared/src/testing/api-test-utils.ts index 8498b6f..e498c1e 100644 --- a/shared/src/testing/api-test-utils.ts +++ b/shared/src/testing/api-test-utils.ts @@ -13,7 +13,13 @@ import type { TicTacToeGameState, RPSGameState, TicTacToeMove, RPSMove } from '. * Generic game mock factory * Creates a mock game instance with all required methods */ -export function createGameMock() { +export function createGameMock(): { + getInitialState: ReturnType; + validateMove: ReturnType; + applyMove: ReturnType; + checkGameEnd: ReturnType; + getValidMoves: ReturnType; +} { return { getInitialState: vi.fn(), validateMove: vi.fn(), @@ -95,7 +101,10 @@ export function createMockGameSession(gameState: T, gam * Vitest mock configuration for shared package games * Use this to create consistent mocks across API route tests */ -export function createSharedGameMocks(gameClass: string) { +export function createSharedGameMocks(gameClass: string): { + mockImplementation: any; + mockGame: ReturnType; +} { const mockGame = createGameMock() return { @@ -110,7 +119,7 @@ export function createSharedGameMocks(gameClass: string) { /** * Create storage function mocks for a specific game type */ -export function createStorageMocks(gameType: GameType) { +export function createStorageMocks(gameType: GameType): Record> { if (gameType === 'tic-tac-toe') { return { getTicTacToeGame: vi.fn(), diff --git a/shared/src/testing/test-database.ts b/shared/src/testing/test-database.ts index 764ebe8..607861a 100644 --- a/shared/src/testing/test-database.ts +++ b/shared/src/testing/test-database.ts @@ -51,7 +51,7 @@ export async function setupTestDatabase( } // Create tables synchronously to ensure they exist before tests run - const createTables = async () => { + const createTables = async (): Promise => { try { await createTicTacToeTable() await createRPSTable() diff --git a/shared/src/testing/vitest-setup.ts b/shared/src/testing/vitest-setup.ts index 09b8af3..56473d2 100644 --- a/shared/src/testing/vitest-setup.ts +++ b/shared/src/testing/vitest-setup.ts @@ -12,7 +12,7 @@ import { setupTestDatabase, teardownTestDatabase } from './test-database' * Standard database setup for all packages * Call this in vitest.setup.ts files to ensure consistent database setup */ -export function setupStandardTestDatabase() { +export function setupStandardTestDatabase(): void { // Setup test database before all tests beforeAll(async () => { try { diff --git a/shared/src/types/games.ts b/shared/src/types/games.ts index 2cb874b..06affdd 100644 --- a/shared/src/types/games.ts +++ b/shared/src/types/games.ts @@ -1,4 +1,4 @@ -import { BaseGameState, GameMove } from './game'; +import { BaseGameState } from './game'; // Tic-tac-toe specific types export type CellValue = 'X' | 'O' | null; From d91b6bc82eac17457462006003fca15752c43de4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:51:26 +0000 Subject: [PATCH 25/37] Complete ESLint fixes - resolved 18 of 26 issues in shared workspace Co-authored-by: chrisreddington <791642+chrisreddington@users.noreply.github.com> --- shared/src/storage/mcp-api-client.ts | 14 +++++++------- shared/src/testing/api-test-utils.ts | 2 +- shared/src/types/game.ts | 4 ++-- shared/src/utils/http-client.ts | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/shared/src/storage/mcp-api-client.ts b/shared/src/storage/mcp-api-client.ts index 576f19a..0a5d7cc 100644 --- a/shared/src/storage/mcp-api-client.ts +++ b/shared/src/storage/mcp-api-client.ts @@ -1,5 +1,5 @@ -import type { GameSession } from '../types/game' -import type { TicTacToeGameState, RPSGameState } from '../types/games' +import type { GameSession, Player } from '../types/game' +import type { TicTacToeGameState, RPSGameState, TicTacToeMove, RPSMove } from '../types/games' import { httpGet, httpPost, WEB_API_BASE } from '../utils/http-client' // MCP-specific API functions that make HTTP calls to web app @@ -7,14 +7,14 @@ export async function getTicTacToeGameForMCP(gameId: string): Promise game.gameState?.id === gameId) + return games.find((game: GameSession) => game.gameState?.id === gameId) } catch (error) { console.error('Error fetching tic-tac-toe game via API:', error) return undefined } } -export async function createTicTacToeGameForMCP(players: any[]): Promise> { +export async function createTicTacToeGameForMCP(players: Player[]): Promise> { try { const playerName = players.find(p => !p.isAI)?.name || 'Player' return await httpPost(`${WEB_API_BASE}/api/games/tic-tac-toe`, { playerName }) @@ -24,7 +24,7 @@ export async function createTicTacToeGameForMCP(players: any[]): Promise> { +export async function makeTicTacToeMove(gameId: string, move: TicTacToeMove, playerId: string): Promise> { try { return await httpPost(`${WEB_API_BASE}/api/games/tic-tac-toe/${gameId}/move`, { move, playerId }) } catch (error) { @@ -36,14 +36,14 @@ export async function makeTicTacToeMove(gameId: string, move: any, playerId: str export async function getRPSGameForMCP(gameId: string): Promise | undefined> { try { const games = await httpGet(`${WEB_API_BASE}/api/games/rock-paper-scissors/mcp`) - return games.find((game: any) => game.gameState?.id === gameId) + return games.find((game: GameSession) => game.gameState?.id === gameId) } catch (error) { console.error('Error fetching RPS game via API:', error) return undefined } } -export async function makeRPSMove(gameId: string, move: any, playerId: string): Promise> { +export async function makeRPSMove(gameId: string, move: RPSMove, playerId: string): Promise> { try { return await httpPost(`${WEB_API_BASE}/api/games/rock-paper-scissors/${gameId}/move`, { move, playerId }) } catch (error) { diff --git a/shared/src/testing/api-test-utils.ts b/shared/src/testing/api-test-utils.ts index e498c1e..d1d38b5 100644 --- a/shared/src/testing/api-test-utils.ts +++ b/shared/src/testing/api-test-utils.ts @@ -102,7 +102,7 @@ export function createMockGameSession(gameState: T, gam * Use this to create consistent mocks across API route tests */ export function createSharedGameMocks(gameClass: string): { - mockImplementation: any; + mockImplementation: Record; mockGame: ReturnType; } { const mockGame = createGameMock() diff --git a/shared/src/types/game.ts b/shared/src/types/game.ts index aa837d2..8b25525 100644 --- a/shared/src/types/game.ts +++ b/shared/src/types/game.ts @@ -19,7 +19,7 @@ export interface BaseGameState { updatedAt: Date; } -export interface GameMove { +export interface GameMove { playerId: PlayerId; move: T; timestamp: Date; @@ -36,7 +36,7 @@ export interface Game { applyMove(gameState: TGameState, move: TMove, playerId: PlayerId): TGameState; checkGameEnd(gameState: TGameState): GameResult | null; getValidMoves(gameState: TGameState, playerId: PlayerId): TMove[]; - getInitialState(players: Player[], options?: any): TGameState; + getInitialState(players: Player[], options?: Record): TGameState; } // Generic game session for API communication diff --git a/shared/src/utils/http-client.ts b/shared/src/utils/http-client.ts index 323ea90..70d4928 100644 --- a/shared/src/utils/http-client.ts +++ b/shared/src/utils/http-client.ts @@ -15,7 +15,7 @@ export const WEB_API_BASE = process.env.WEB_API_BASE || 'http://localhost:3000' * @returns Promise resolving to the JSON response * @throws Error if the request fails or returns non-2xx status */ -export async function httpGet(url: string): Promise { +export async function httpGet(url: string): Promise { const response = await fetch(url) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) @@ -31,7 +31,7 @@ export async function httpGet(url: string): Promise { * @returns Promise resolving to the JSON response * @throws Error if the request fails or returns non-2xx status */ -export async function httpPost(url: string, data: any): Promise { +export async function httpPost(url: string, data: unknown): Promise { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, From be70dc81fab02661fbdada50e5693764497ef9bc Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 19:10:53 +0100 Subject: [PATCH 26/37] Fix linting errors for MCP workspace --- .../src/ai/rock-paper-scissors-ai.test.ts | 30 ++--- mcp-server/src/ai/rock-paper-scissors-ai.ts | 4 +- .../ai/rock-paper-scissors-security.test.ts | 4 +- mcp-server/src/ai/tic-tac-toe-ai.test.ts | 36 +++--- mcp-server/src/ai/tic-tac-toe-ai.ts | 8 +- .../src/handlers/elicitation-handlers.ts | 22 ++-- mcp-server/src/handlers/game-operations.ts | 121 ++++++++++-------- mcp-server/src/handlers/prompt-handlers.ts | 10 +- mcp-server/src/handlers/resource-handlers.ts | 8 +- mcp-server/src/handlers/tool-handlers.ts | 44 +++---- .../src/integration/integration.test.ts | 23 ++-- mcp-server/src/server.ts | 4 +- mcp-server/src/utils/http-client.ts | 24 ++-- mcp-server/vitest.setup.ts | 2 +- 14 files changed, 179 insertions(+), 161 deletions(-) diff --git a/mcp-server/src/ai/rock-paper-scissors-ai.test.ts b/mcp-server/src/ai/rock-paper-scissors-ai.test.ts index 6a566f8..830ba91 100644 --- a/mcp-server/src/ai/rock-paper-scissors-ai.test.ts +++ b/mcp-server/src/ai/rock-paper-scissors-ai.test.ts @@ -1,4 +1,4 @@ -import { RockPaperScissorsAI, type Strategy, type Difficulty } from './rock-paper-scissors-ai.js'; +import { RockPaperScissorsAI } from './rock-paper-scissors-ai.js'; import type { RPSGameState, RPSChoice, Player } from '@turn-based-mcp/shared'; import { RockPaperScissorsGame } from '@turn-based-mcp/shared'; @@ -59,8 +59,8 @@ describe('RockPaperScissorsAI', () => { }); it('should counter the most frequent opponent choice', async () => { - // Create a state where opponent played rock 2 times, paper 1 time - let state = initialState; + // Create a state where opponent played rock 2 times, paper 1 time + let state = initialState; state = game.applyMove(state, { choice: 'rock' }, 'player1'); state = game.applyMove(state, { choice: 'paper' }, 'ai'); state = game.applyMove(state, { choice: 'rock' }, 'player1'); @@ -74,8 +74,8 @@ describe('RockPaperScissorsAI', () => { }); it('should handle tied frequencies', async () => { - // Create equal frequency scenario - let state = initialState; + // Create equal frequency scenario + let state = initialState; state = game.applyMove(state, { choice: 'rock' }, 'player1'); state = game.applyMove(state, { choice: 'paper' }, 'ai'); state = game.applyMove(state, { choice: 'paper' }, 'player1'); @@ -126,7 +126,7 @@ describe('RockPaperScissorsAI', () => { }); it('should default to random for invalid difficulty', async () => { - const choice = await ai.makeChoice(initialState, 'invalid' as Difficulty); + const choice = await ai.makeChoice(initialState, 'invalid' as any); expect(['rock', 'paper', 'scissors']).toContain(choice); }); }); @@ -156,7 +156,7 @@ describe('RockPaperScissorsAI', () => { }); describe('analyzeGameState', () => { - it('should provide basic game analysis for initial state', () => { + it('should provide basic game analysis for initial state', () => { const analysis = ai.analyzeGameState(initialState); expect(analysis).toContain('Game Status: playing'); @@ -165,7 +165,7 @@ describe('RockPaperScissorsAI', () => { }); it('should show round history after moves', async () => { - let state = initialState; + let state = initialState; state = game.applyMove(state, { choice: 'rock' }, 'player1'); state = game.applyMove(state, { choice: 'paper' }, 'ai'); @@ -175,7 +175,7 @@ describe('RockPaperScissorsAI', () => { }); it('should analyze opponent patterns', async () => { - let state = initialState; + let state = initialState; // Create pattern: rock, paper, rock state = game.applyMove(state, { choice: 'rock' }, 'player1'); state = game.applyMove(state, { choice: 'scissors' }, 'ai'); @@ -196,7 +196,7 @@ describe('RockPaperScissorsAI', () => { }); it('should show current score correctly', async () => { - let state = initialState; + let state = initialState; // reassigned in subsequent applyMove calls // Player 1 wins first round state = game.applyMove(state, { choice: 'rock' }, 'player1'); state = game.applyMove(state, { choice: 'scissors' }, 'ai'); @@ -208,7 +208,7 @@ describe('RockPaperScissorsAI', () => { }); it('should handle finished games', () => { - let state = initialState; + const state = { ...initialState }; // mutate copy, no reassignment afterward state.status = 'finished'; state.winner = 'player1'; state.currentRound = 3; @@ -218,7 +218,7 @@ describe('RockPaperScissorsAI', () => { }); it('should handle draw rounds', async () => { - let state = initialState; + let state = initialState; // reassigned in applyMove calls state = game.applyMove(state, { choice: 'rock' }, 'player1'); state = game.applyMove(state, { choice: 'rock' }, 'player2'); @@ -278,7 +278,7 @@ describe('RockPaperScissorsAI', () => { }); it('should maintain consistent difficulty behavior', async () => { - const difficulties: Difficulty[] = ['easy', 'medium', 'hard']; + const difficulties: any[] = ['easy', 'medium', 'hard']; for (const difficulty of difficulties) { const choice = await ai.makeChoice(initialState, difficulty); @@ -287,7 +287,7 @@ describe('RockPaperScissorsAI', () => { }); it('should handle incomplete rounds', () => { - let state = initialState; + const state = initialState; // Only player1 has made a choice in current round state.rounds[0] = { player1Choice: 'rock' }; @@ -307,7 +307,7 @@ describe('RockPaperScissorsAI', () => { it('should be consistent with same game state', async () => { // Create a deterministic scenario - let state = initialState; + let state = initialState; state = game.applyMove(state, { choice: 'rock' }, 'player1'); state = game.applyMove(state, { choice: 'paper' }, 'player2'); state = game.applyMove(state, { choice: 'rock' }, 'player1'); diff --git a/mcp-server/src/ai/rock-paper-scissors-ai.ts b/mcp-server/src/ai/rock-paper-scissors-ai.ts index a31e446..c6a2b1d 100644 --- a/mcp-server/src/ai/rock-paper-scissors-ai.ts +++ b/mcp-server/src/ai/rock-paper-scissors-ai.ts @@ -1,4 +1,4 @@ -import type { RPSGameState, RPSMove, RPSChoice, Difficulty } from '@turn-based-mcp/shared' +import type { RPSGameState, RPSChoice, Difficulty } from '@turn-based-mcp/shared' import { RockPaperScissorsGame } from '@turn-based-mcp/shared' export type Strategy = 'random' | 'adaptive' | 'pattern' @@ -195,7 +195,7 @@ export class RockPaperScissorsAI { * Rebuilds the opponent history array from the game state's completed rounds. * Assumes player1 is the human opponent whose patterns we want to learn. */ - private updateOpponentHistory(gameState: RPSGameState) { + private updateOpponentHistory(gameState: RPSGameState): void { // Extract opponent moves from completed rounds this.opponentHistory = [] diff --git a/mcp-server/src/ai/rock-paper-scissors-security.test.ts b/mcp-server/src/ai/rock-paper-scissors-security.test.ts index ab32009..e819326 100644 --- a/mcp-server/src/ai/rock-paper-scissors-security.test.ts +++ b/mcp-server/src/ai/rock-paper-scissors-security.test.ts @@ -1,10 +1,8 @@ import { RockPaperScissorsAI } from './rock-paper-scissors-ai' -import { RockPaperScissorsGame } from '@turn-based-mcp/shared' import type { RPSGameState, RPSChoice } from '@turn-based-mcp/shared' describe('RockPaperScissorsAI - Current Move Security', () => { let ai: RockPaperScissorsAI - let game: RockPaperScissorsGame const createMockGameState = ( rounds: Array<{ @@ -31,7 +29,7 @@ describe('RockPaperScissorsAI - Current Move Security', () => { beforeEach(() => { ai = new RockPaperScissorsAI() - game = new RockPaperScissorsGame() + // separate game instance not required for these security-focused tests }) describe('Current Round Security Tests', () => { diff --git a/mcp-server/src/ai/tic-tac-toe-ai.test.ts b/mcp-server/src/ai/tic-tac-toe-ai.test.ts index 65e898a..24a555a 100644 --- a/mcp-server/src/ai/tic-tac-toe-ai.test.ts +++ b/mcp-server/src/ai/tic-tac-toe-ai.test.ts @@ -1,4 +1,4 @@ -import { TicTacToeAI, type Difficulty } from './tic-tac-toe-ai.js'; +import { TicTacToeAI } from './tic-tac-toe-ai.js'; import type { TicTacToeGameState, Player } from '@turn-based-mcp/shared'; import { TicTacToeGame } from '@turn-based-mcp/shared'; @@ -35,7 +35,7 @@ describe('TicTacToeAI', () => { it('should choose from available moves only', async () => { // Fill most of the board - let state = initialState; + const state = initialState; state.board = [ ['X', 'O', 'X'], ['O', 'X', 'O'], @@ -61,7 +61,7 @@ describe('TicTacToeAI', () => { describe('medium difficulty', () => { it('should win when possible', async () => { - let state = initialState; + const state = initialState; // Set up a winning scenario for AI state.board = [ ['O', 'O', null], @@ -74,7 +74,7 @@ describe('TicTacToeAI', () => { }); it('should block opponent from winning', async () => { - let state = initialState; + const state = initialState; // Set up scenario where player can win state.board = [ ['X', 'X', null], @@ -101,7 +101,7 @@ describe('TicTacToeAI', () => { }); it('should prefer center when available', async () => { - let state = initialState; + const state = initialState; state.board = [ ['X', null, null], [null, null, null], @@ -113,7 +113,7 @@ describe('TicTacToeAI', () => { }); it('should choose corners when center is taken', async () => { - let state = initialState; + const state = initialState; state.board = [ [null, null, null], [null, 'X', null], @@ -130,7 +130,7 @@ describe('TicTacToeAI', () => { }); it('should prioritize winning over blocking', async () => { - let state = initialState; + const state = initialState; // Both AI and player can win state.board = [ ['O', 'O', null], // AI can win here @@ -145,7 +145,7 @@ describe('TicTacToeAI', () => { describe('hard difficulty', () => { it('should play optimally using minimax', async () => { - let state = initialState; + const state = initialState; // Classic opening: AI should respond optimally to corner play state.board = [ ['X', null, null], @@ -158,7 +158,7 @@ describe('TicTacToeAI', () => { }); it('should never lose from a winning position', async () => { - let state = initialState; + const state = initialState; // AI is in a winning position state.board = [ ['O', 'X', null], @@ -171,7 +171,7 @@ describe('TicTacToeAI', () => { }); it('should force a draw from losing position', async () => { - let state = initialState; + const state = initialState; // Player has advantage but AI should force draw state.board = [ ['X', null, null], @@ -206,7 +206,7 @@ describe('TicTacToeAI', () => { }); it('should default to easy difficulty for invalid difficulty', async () => { - const move = await ai.makeMove(initialState, 'invalid' as Difficulty); + const move = await ai.makeMove(initialState, 'invalid' as any); expect(game.validateMove(initialState, move, 'ai')).toBe(true); }); }); @@ -223,7 +223,7 @@ describe('TicTacToeAI', () => { }); it('should detect winning opportunities', () => { - let state = initialState; + const state = initialState; state.board = [ ['O', 'O', null], ['X', null, null], @@ -235,7 +235,7 @@ describe('TicTacToeAI', () => { }); it('should detect player threats', () => { - let state = initialState; + const state = initialState; state.board = [ ['X', 'X', null], ['O', null, null], @@ -250,7 +250,7 @@ describe('TicTacToeAI', () => { }); it('should analyze board state correctly', () => { - let state = initialState; + const state = initialState; state.board = [ ['X', null, 'O'], [null, 'X', null], // Center has X @@ -264,7 +264,7 @@ describe('TicTacToeAI', () => { }); it('should handle game-ending scenarios', () => { - let state = initialState; + const state = initialState; state.board = [ ['X', 'X', 'X'], ['O', 'O', null], @@ -280,7 +280,7 @@ describe('TicTacToeAI', () => { describe('edge cases and robustness', () => { it('should handle full board gracefully', async () => { - let state = initialState; + const state = initialState; state.board = [ ['X', 'O', 'X'], ['O', 'X', 'O'], @@ -304,7 +304,7 @@ describe('TicTacToeAI', () => { }); it('should maintain consistent behavior across difficulties', async () => { - const difficulties: Difficulty[] = ['easy', 'medium', 'hard']; + const difficulties: any[] = ['easy', 'medium', 'hard']; for (const difficulty of difficulties) { const move = await ai.makeMove(initialState, difficulty); @@ -313,7 +313,7 @@ describe('TicTacToeAI', () => { }); it('should handle near-end game scenarios', async () => { - let state = initialState; + const state = initialState; // Only one move left state.board = [ ['X', 'O', 'X'], diff --git a/mcp-server/src/ai/tic-tac-toe-ai.ts b/mcp-server/src/ai/tic-tac-toe-ai.ts index a3d7e50..78c4493 100644 --- a/mcp-server/src/ai/tic-tac-toe-ai.ts +++ b/mcp-server/src/ai/tic-tac-toe-ai.ts @@ -1,4 +1,4 @@ -import type { TicTacToeGameState, TicTacToeMove, Difficulty } from '@turn-based-mcp/shared' +import type { TicTacToeGameState, TicTacToeMove, Difficulty, PlayerId } from '@turn-based-mcp/shared' import { TicTacToeGame } from '@turn-based-mcp/shared' /** @@ -193,11 +193,11 @@ export class TicTacToeAI { * Tests each valid move to see if it results in an immediate win. * Used for both finding AI wins and blocking opponent wins. */ - private findWinningMove(gameState: TicTacToeGameState, playerId: string): TicTacToeMove | null { - const validMoves = this.game.getValidMoves(gameState, playerId as any) + private findWinningMove(gameState: TicTacToeGameState, playerId: PlayerId): TicTacToeMove | null { + const validMoves = this.game.getValidMoves(gameState, playerId) for (const move of validMoves) { - const tempGameState = this.game.applyMove(gameState, move, playerId as any) + const tempGameState = this.game.applyMove(gameState, move, playerId) const result = this.game.checkGameEnd(tempGameState) if (result && result.winner === playerId) { diff --git a/mcp-server/src/handlers/elicitation-handlers.ts b/mcp-server/src/handlers/elicitation-handlers.ts index be5ed7f..ef47d3f 100644 --- a/mcp-server/src/handlers/elicitation-handlers.ts +++ b/mcp-server/src/handlers/elicitation-handlers.ts @@ -7,16 +7,16 @@ import { DIFFICULTIES, GAME_TYPES, DEFAULT_PLAYER_NAME, DEFAULT_AI_DIFFICULTY } export interface ElicitationResult { action: "accept" | "decline" | "cancel" - content?: Record + content?: Record } /** * Game creation preferences elicitation */ export async function elicitGameCreationPreferences( - server: any, + server: { elicitInput: (args: { message: string; requestedSchema: unknown }) => Promise }, gameType: string, - existingArgs?: Record + existingArgs?: Record ): Promise { const schemas = { 'tic-tac-toe': { @@ -78,8 +78,8 @@ export async function elicitGameCreationPreferences( } // Filter out properties that are already provided - const filteredSchema: any = { ...baseSchema } - const filteredProperties: Record = { ...baseSchema.properties } + const filteredSchema: Record = { ...baseSchema } + const filteredProperties: Record = { ...baseSchema.properties } const filteredRequired = [...(baseSchema.required || [])] // Remove properties that already have values @@ -140,7 +140,7 @@ export async function elicitGameCreationPreferences( * Mid-game decision elicitation */ export async function elicitMidGameDecision( - server: any, + server: { elicitInput: (args: { message: string; requestedSchema: unknown }) => Promise }, context: { gameType: string gameId: string @@ -148,7 +148,7 @@ export async function elicitMidGameDecision( options: Array<{ value: string; label: string; description?: string }> } ): Promise { - const { gameType, gameId, situation, options } = context + const { gameType, situation, options } = context const schema = { type: "object", @@ -190,7 +190,7 @@ export async function elicitMidGameDecision( * Game completion feedback elicitation */ export async function elicitGameCompletionFeedback( - server: any, + server: { elicitInput: (args: { message: string; requestedSchema: unknown }) => Promise }, context: { gameType: string gameId: string @@ -257,7 +257,7 @@ export async function elicitGameCompletionFeedback( * Strategy hint elicitation */ export async function elicitStrategyPreference( - server: any, + server: { elicitInput: (args: { message: string; requestedSchema: unknown }) => Promise }, context: { gameType: string gameId: string @@ -265,7 +265,7 @@ export async function elicitStrategyPreference( currentSituation: string } ): Promise { - const { gameType, availableHints, currentSituation } = context + const { currentSituation } = context const schema = { type: "object", @@ -311,7 +311,7 @@ export async function elicitStrategyPreference( * Error recovery elicitation */ export async function elicitErrorRecovery( - server: any, + server: { elicitInput: (args: { message: string; requestedSchema: unknown }) => Promise }, context: { gameType: string gameId: string diff --git a/mcp-server/src/handlers/game-operations.ts b/mcp-server/src/handlers/game-operations.ts index 04b9d0b..6f2096d 100644 --- a/mcp-server/src/handlers/game-operations.ts +++ b/mcp-server/src/handlers/game-operations.ts @@ -4,7 +4,7 @@ import { TicTacToeAI } from '../ai/tic-tac-toe-ai.js' import { RockPaperScissorsAI } from '../ai/rock-paper-scissors-ai.js' -import { TicTacToeGame, RockPaperScissorsGame } from '@turn-based-mcp/shared' +import { TicTacToeGame, type TicTacToeGameState, type RPSGameState } from '@turn-based-mcp/shared' import { getGameViaAPI, submitMoveViaAPI, createGameViaAPI } from '../utils/http-client.js' import { DEFAULT_PLAYER_NAME, DEFAULT_AI_DIFFICULTY } from '@turn-based-mcp/shared' @@ -12,12 +12,14 @@ import { DEFAULT_PLAYER_NAME, DEFAULT_AI_DIFFICULTY } from '@turn-based-mcp/shar const ticTacToeAI = new TicTacToeAI() const rpsAI = new RockPaperScissorsAI() const ticTacToeGame = new TicTacToeGame() -const rpsGame = new RockPaperScissorsGame() +// rpsGame instance not needed directly in this module (AI handles logic) /** * Helper function to read game resource */ -export async function readGameResource(gameType: string, gameId: string) { +type SupportedDifficulty = 'easy' | 'medium' | 'hard' +interface GameSessionWrapper { gameState: TicTacToeGameState | RPSGameState; difficulty?: SupportedDifficulty; history?: unknown[] } +export async function readGameResource(gameType: string, gameId: string): Promise { const uri = `game://${gameType}/${gameId}` try { const gameSession = await getGameViaAPI(gameType, gameId) @@ -33,7 +35,7 @@ export async function readGameResource(gameType: string, gameId: string) { /** * Generic play game function */ -export async function playGame(gameType: string, gameId: string) { +export async function playGame(gameType: string, gameId: string): Promise> { // Get current game state via resource const gameSession = await readGameResource(gameType, gameId) @@ -50,21 +52,23 @@ export async function playGame(gameType: string, gameId: string) { throw new Error(`Game is not in playing state. Current status: ${gameSession.gameState.status}`) } - let aiMove: any + let aiMove: { row: number; col: number } | { choice: string } | undefined let moveDescription: string // Calculate AI move based on game type switch (gameType) { - case 'tic-tac-toe': - aiMove = await ticTacToeAI.makeMove(gameSession.gameState, difficulty as any) - moveDescription = `AI made move at row ${aiMove.row + 1}, col ${aiMove.col + 1}` + case 'tic-tac-toe': { + const move = await ticTacToeAI.makeMove(gameSession.gameState as TicTacToeGameState, difficulty as SupportedDifficulty) + aiMove = move + moveDescription = `AI made move at row ${move.row + 1}, col ${move.col + 1}` break - - case 'rock-paper-scissors': - const aiChoice = await rpsAI.makeChoice(gameSession.gameState, difficulty as any) + } + case 'rock-paper-scissors': { + const aiChoice = await rpsAI.makeChoice(gameSession.gameState as RPSGameState, difficulty as SupportedDifficulty) aiMove = { choice: aiChoice } moveDescription = `AI chose ${aiMove.choice}` break + } default: throw new Error(`Unsupported game type: ${gameType}`) @@ -74,7 +78,7 @@ export async function playGame(gameType: string, gameId: string) { const updatedGameSession = await submitMoveViaAPI(gameType, gameId, aiMove, 'ai') // Format response based on game type - const response: any = { + const response: Record = { gameId, gameType, difficulty, @@ -88,7 +92,9 @@ export async function playGame(gameType: string, gameId: string) { // Add game-specific move details switch (gameType) { case 'tic-tac-toe': - response.aiMove = { row: aiMove.row, col: aiMove.col } + if (aiMove && 'row' in aiMove) { + response.aiMove = { row: aiMove.row, col: aiMove.col } + } break case 'rock-paper-scissors': response.aiMove = aiMove @@ -106,13 +112,13 @@ export async function playGame(gameType: string, gameId: string) { /** * Generic analyze game function */ -export async function analyzeGame(gameType: string, gameId: string) { +export async function analyzeGame(gameType: string, gameId: string): Promise> { // Get current game state via resource const gameSession = await readGameResource(gameType, gameId) - const gameState = gameSession.gameState + const gameState = gameSession.gameState as TicTacToeGameState | RPSGameState const history = gameSession.history || [] - let analysis: any = { + const analysis: { [k: string]: unknown } = { gameId, gameType, status: gameState.status, @@ -126,20 +132,23 @@ export async function analyzeGame(gameType: string, gameId: string) { switch (gameType) { case 'tic-tac-toe': - analysis.boardState = gameState.board - analysis.playerSymbols = gameState.playerSymbols - analysis.validMoves = gameState.status === 'playing' ? ticTacToeGame.getValidMoves(gameState, gameState.currentPlayerId) : [] + { + const tState = gameState as TicTacToeGameState + analysis.boardState = tState.board + analysis.playerSymbols = tState.playerSymbols + analysis.validMoves = tState.status === 'playing' ? ticTacToeGame.getValidMoves(tState, tState.currentPlayerId) : [] if (gameState.status === 'playing') { - analysisText += `Current Turn: ${gameState.currentPlayerId} (${gameState.playerSymbols[gameState.currentPlayerId]})\n` - analysisText += `Valid Moves: ${analysis.validMoves.length} available\n` + analysisText += `Current Turn: ${tState.currentPlayerId} (${tState.playerSymbols[tState.currentPlayerId]})\n` + const validMoves = analysis.validMoves as Array + analysisText += `Valid Moves: ${validMoves.length} available\n` // Board visualization analysisText += '\nCurrent Board:\n' for (let row = 0; row < 3; row++) { let rowStr = '' for (let col = 0; col < 3; col++) { - const cell = gameState.board[row][col] + const cell = tState.board[row][col] rowStr += cell ? ` ${cell} ` : ' ' if (col < 2) rowStr += '|' } @@ -147,38 +156,41 @@ export async function analyzeGame(gameType: string, gameId: string) { if (row < 2) analysisText += '-----------\n' } - analysisText += gameState.currentPlayerId === 'ai' + analysisText += tState.currentPlayerId === 'ai' ? '\nIt\'s the AI\'s turn to move.' : '\nWaiting for human player to make a move.' - } else if (gameState.status === 'finished') { - analysisText += `Winner: ${gameState.winner || 'Draw'}\n` + } else if (tState.status === 'finished') { + analysisText += `Winner: ${tState.winner || 'Draw'}\n` analysisText += `Total moves played: ${history.length}\n` } + } break case 'rock-paper-scissors': - analysis.currentRound = gameState.currentRound - analysis.maxRounds = gameState.maxRounds - analysis.scores = gameState.scores - analysis.rounds = gameState.rounds + { + const rState = gameState as RPSGameState + analysis.currentRound = rState.currentRound + analysis.maxRounds = rState.maxRounds + analysis.scores = rState.scores + analysis.rounds = rState.rounds - analysisText += `Round: ${gameState.currentRound}/${gameState.maxRounds}\n` - analysisText += `Scores: Player: ${gameState.scores.player1 || 0}, AI: ${gameState.scores.ai || 0}\n\n` + analysisText += `Round: ${rState.currentRound}/${rState.maxRounds}\n` + analysisText += `Scores: Player: ${rState.scores.player1 || 0}, AI: ${rState.scores.ai || 0}\n\n` - if (gameState.status === 'playing') { - analysisText += `Current Turn: ${gameState.currentPlayerId}\n` + if (rState.status === 'playing') { + analysisText += `Current Turn: ${rState.currentPlayerId}\n` - const currentRoundData = gameState.rounds[gameState.currentRound - 1] - if (currentRoundData) { + const currentRoundData = rState.rounds[rState.currentRound - 1] + if (currentRoundData) { const player1HasChoice = !!currentRoundData.player1Choice const player2HasChoice = !!currentRoundData.player2Choice if (player1HasChoice && player2HasChoice) { - analysisText += `Round ${gameState.currentRound} Choices:\n` + analysisText += `Round ${rState.currentRound} Choices:\n` analysisText += `- Player: ${currentRoundData.player1Choice}\n` analysisText += `- AI: ${currentRoundData.player2Choice}\n` } else { - analysisText += `Round ${gameState.currentRound} Status:\n` + analysisText += `Round ${rState.currentRound} Status:\n` if (player1HasChoice || player2HasChoice) { analysisText += `- Some players have made their choices\n` } else { @@ -187,19 +199,20 @@ export async function analyzeGame(gameType: string, gameId: string) { } } - analysisText += gameState.currentPlayerId === 'ai' + analysisText += rState.currentPlayerId === 'ai' ? '\nIt\'s the AI\'s turn to make a choice.' : '\nWaiting for human player to make a choice.' - } else if (gameState.status === 'finished') { - analysisText += `Winner: ${gameState.winner || 'Draw'}\n` - analysisText += `Total rounds played: ${gameState.rounds.length}\n` + } else if (rState.status === 'finished') { + analysisText += `Winner: ${rState.winner || 'Draw'}\n` + analysisText += `Total rounds played: ${rState.rounds.length}\n` - analysisText += `\nRound Results:\n` - gameState.rounds.forEach((round: any, index: number) => { - if (round.player1Choice && round.player2Choice) { - analysisText += `Round ${index + 1}: Player (${round.player1Choice}) vs AI (${round.player2Choice}) - ${round.winner === 'draw' ? 'Draw' : `Winner: ${round.winner}`}\n` - } - }) + analysisText += `\nRound Results:\n` + rState.rounds.forEach((round, index: number) => { + if (round.player1Choice && round.player2Choice) { + analysisText += `Round ${index + 1}: Player (${round.player1Choice}) vs AI (${round.player2Choice}) - ${round.winner === 'draw' ? 'Draw' : `Winner: ${round.winner}`}\n` + } + }) + } } break @@ -219,7 +232,7 @@ export async function waitForPlayerMove( gameId: string, timeoutSeconds: number = 15, pollInterval: number = 3 -) { +): Promise> { const gameTypeNames: { [key: string]: string } = { 'tic-tac-toe': 'Tic-Tac-Toe', 'rock-paper-scissors': 'Rock Paper Scissors' @@ -231,7 +244,7 @@ export async function waitForPlayerMove( } // Get initial game state via resource - let currentGameSession = await readGameResource(gameType, gameId) + const currentGameSession = await readGameResource(gameType, gameId) // Check if game is finished if (currentGameSession.gameState.status === 'finished') { @@ -320,14 +333,14 @@ export async function createGame( playerName: string = DEFAULT_PLAYER_NAME, gameId?: string, difficulty: string = DEFAULT_AI_DIFFICULTY, - gameSpecificOptions?: Record -) { + gameSpecificOptions?: Record +): Promise> { // Check if game already exists (for games that support custom IDs) if (gameId && gameType === 'tic-tac-toe') { try { const existingGame = await readGameResource(gameType, gameId) if (existingGame) { - const response: any = { + const response: Record = { gameId, gameType, message: `Found existing ${gameType} game with ID: ${gameId}`, @@ -337,7 +350,7 @@ export async function createGame( return response } - } catch (error) { + } catch { // Game doesn't exist, continue with creation } } @@ -345,7 +358,7 @@ export async function createGame( // Create new game via API const gameSession = await createGameViaAPI(gameType, playerName, gameId, difficulty, gameSpecificOptions) - const response: any = { + const response: Record = { gameId: gameSession.gameState.id, gameType, gameState: gameSession.gameState, diff --git a/mcp-server/src/handlers/prompt-handlers.ts b/mcp-server/src/handlers/prompt-handlers.ts index 445321b..3e7e1ec 100644 --- a/mcp-server/src/handlers/prompt-handlers.ts +++ b/mcp-server/src/handlers/prompt-handlers.ts @@ -14,7 +14,7 @@ export interface PromptDefinition { description: string required?: boolean }> - handler: (args?: Record) => Promise<{ + handler: (args?: Record) => Promise<{ description?: string messages: PromptMessage[] }> @@ -97,9 +97,9 @@ export const STRATEGY_PROMPTS: PromptDefinition[] = [ required: false } ], - handler: async (args = {}) => { - const gameType = args.gameType?.toLowerCase() - const difficulty = args.difficulty?.toLowerCase() + handler: async (args: Record = {}): Promise<{ description: string; messages: PromptMessage[] }> => { + const gameType = typeof args.gameType === 'string' ? args.gameType.toLowerCase() : undefined + const difficulty = typeof args.difficulty === 'string' ? args.difficulty.toLowerCase() : undefined let content = `# AI Difficulty Strategy Guide\n\n` @@ -266,7 +266,7 @@ export async function listPrompts(): Promise<{ prompts: Prompt[] }> { } } -export async function getPrompt(name: string, args?: Record) { +export async function getPrompt(name: string, args?: Record): Promise<{ description?: string; messages: PromptMessage[] }> { const prompt = ALL_PROMPTS.find(p => p.name === name) if (!prompt) { throw new Error(`Prompt not found: ${name}`) diff --git a/mcp-server/src/handlers/resource-handlers.ts b/mcp-server/src/handlers/resource-handlers.ts index bd1fd03..389bf39 100644 --- a/mcp-server/src/handlers/resource-handlers.ts +++ b/mcp-server/src/handlers/resource-handlers.ts @@ -8,7 +8,7 @@ import { GAME_TYPES, isSupportedGameType } from '@turn-based-mcp/shared' /** * List all available game resources */ -export async function listResources() { +export async function listResources(): Promise<{ resources: Array> }> { try { const resources = [] @@ -52,9 +52,9 @@ export async function listResources() { /** * Read a specific game resource */ -export async function readResource(uri: string) { +export async function readResource(uri: string): Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }> { // Parse game resource URI: game://{gameType} or game://{gameType}/{gameId} - const match = uri.match(/^game:\/\/([^\/]+)(?:\/([^\/]+))?$/) + const match = uri.match(/^game:\/\/([^/]+)(?:\/([^/]+))?$/) if (!match) { throw new Error(`Invalid game resource URI: ${uri}`) } @@ -101,7 +101,7 @@ export async function readResource(uri: string) { mimeType: 'application/json', text: JSON.stringify({ gameType, - games: games.map((game: any) => ({ + games: games.map((game: { gameState?: { id?: string; status?: string; currentPlayerId?: string; winner?: string | null; createdAt?: string; updatedAt?: string; players?: Record }; difficulty?: string }) => ({ gameId: game.gameState?.id, status: game.gameState?.status, currentPlayer: game.gameState?.currentPlayerId, diff --git a/mcp-server/src/handlers/tool-handlers.ts b/mcp-server/src/handlers/tool-handlers.ts index d8130ad..2d6c3b7 100644 --- a/mcp-server/src/handlers/tool-handlers.ts +++ b/mcp-server/src/handlers/tool-handlers.ts @@ -118,11 +118,14 @@ export const TOOL_DEFINITIONS = [ /** * Handle tool execution */ -export async function handleToolCall(name: string, args: any, server?: any) { - try { +export interface ServerWithElicit { + elicitInput: (args: { message: string; requestedSchema: unknown }) => Promise<{ action: 'accept' | 'decline' | 'cancel'; content?: Record }> +} + +export async function handleToolCall(name: string, args: Record, server?: ServerWithElicit): Promise { switch (name) { - case 'play_game': - const { gameId: playGameId, gameType: playGameType } = args + case 'play_game': { + const { gameId: playGameId, gameType: playGameType } = args as { gameId?: string; gameType?: string } if (!playGameId) { throw new Error('gameId is required') } @@ -133,9 +136,9 @@ export async function handleToolCall(name: string, args: any, server?: any) { throw new Error(`Unsupported game type: ${playGameType}`) } return await playGame(playGameType, playGameId) - - case 'analyze_game': - const { gameId: analyzeGameId, gameType: analyzeGameType } = args + } + case 'analyze_game': { + const { gameId: analyzeGameId, gameType: analyzeGameType } = args as { gameId?: string; gameType?: string } if (!analyzeGameId) { throw new Error('gameId is required') } @@ -143,14 +146,14 @@ export async function handleToolCall(name: string, args: any, server?: any) { throw new Error('gameType is required') } return await analyzeGame(analyzeGameType, analyzeGameId) - - case 'wait_for_player_move': + } + case 'wait_for_player_move': { const { gameId: waitGameId, gameType: waitGameType, timeoutSeconds = 15, pollInterval = 3 - } = args + } = args as { gameId?: string; gameType?: string; timeoutSeconds?: number; pollInterval?: number } if (!waitGameId) { throw new Error('gameId is required') } @@ -158,29 +161,26 @@ export async function handleToolCall(name: string, args: any, server?: any) { throw new Error('gameType is required') } return await waitForPlayerMove(waitGameType, waitGameId, timeoutSeconds, pollInterval) - - case 'create_game': - const { gameType: genericGameType, gameId: genericGameId } = args + } + case 'create_game': { + const { gameType: genericGameType, gameId: genericGameId } = args as { gameType?: string; gameId?: string } if (!genericGameType) { throw new Error('gameType is required') } if (!isSupportedGameType(genericGameType)) { throw new Error(`Unsupported game type: ${genericGameType}`) } - return await createGameWithElicitation(genericGameType, genericGameId, server, args) - + return await createGameWithElicitation(genericGameType, genericGameId, server, args) + } default: throw new Error(`Unknown tool: ${name}`) } - } catch (error) { - throw error - } } /** * Create game with interactive elicitation */ -async function createGameWithElicitation(gameType: string, gameId?: string, server?: any, toolArgs?: any) { +async function createGameWithElicitation(gameType: string, gameId?: string, server?: ServerWithElicit, toolArgs?: Record): Promise { if (!server) { // Fallback to regular creation if no server for elicitation return await createGame(gameType, DEFAULT_PLAYER_NAME, gameId, DEFAULT_AI_DIFFICULTY) @@ -188,7 +188,7 @@ async function createGameWithElicitation(gameType: string, gameId?: string, serv try { // Elicit user preferences - const elicitationResult = await elicitGameCreationPreferences(server, gameType, { + const elicitationResult = await elicitGameCreationPreferences(server, gameType, { gameId, playerName: toolArgs?.playerName, difficulty: toolArgs?.difficulty, @@ -214,7 +214,7 @@ async function createGameWithElicitation(gameType: string, gameId?: string, serv const finalDifficulty = difficulty || 'medium' // Prepare game-specific options - const gameSpecificOptions: Record = {} + const gameSpecificOptions: Record = {} if (gameType === 'tic-tac-toe' && playerSymbol) { gameSpecificOptions.playerSymbol = playerSymbol } @@ -223,7 +223,7 @@ async function createGameWithElicitation(gameType: string, gameId?: string, serv } // Create the game with elicited preferences - const gameResult = await createGame(gameType, finalPlayerName, gameId, finalDifficulty, gameSpecificOptions) + const gameResult = await createGame(gameType, finalPlayerName, gameId, finalDifficulty, gameSpecificOptions) // Add elicitation information to the response gameResult.elicitation = { diff --git a/mcp-server/src/integration/integration.test.ts b/mcp-server/src/integration/integration.test.ts index da77b94..087f6b1 100644 --- a/mcp-server/src/integration/integration.test.ts +++ b/mcp-server/src/integration/integration.test.ts @@ -5,7 +5,8 @@ import { listPrompts, getPrompt } from '../handlers/prompt-handlers.js' import * as httpClient from '../utils/http-client.js' // Import the real constants from shared package -import { GAME_TYPES, DIFFICULTIES, isSupportedGameType, DEFAULT_PLAYER_NAME, DEFAULT_AI_DIFFICULTY } from '@turn-based-mcp/shared' +// Importing shared constants (some unused intentionally for integration scope) - remove to satisfy lint +// Removed unused imports // Mock the web API calls for testing vi.mock('../utils/http-client.js', () => ({ @@ -66,15 +67,11 @@ describe('MCP Server Integration', () => { expect(result.resources.length).toBeGreaterThan(0) // Should include game type resources - const gameTypeResources = result.resources.filter(r => - r.uri.match(/^game:\/\/[^\/]+$/) - ) + const gameTypeResources = (result.resources as Array<{ uri: string }>).filter(r => r.uri.match(/^game:\/\/[^/]+$/)) expect(gameTypeResources.length).toBe(2) // tic-tac-toe, rock-paper-scissors // Should include individual game resources - const individualGameResources = result.resources.filter(r => - r.uri.match(/^game:\/\/[^\/]+\/[^\/]+$/) - ) + const individualGameResources = (result.resources as Array<{ uri: string }>).filter(r => r.uri.match(/^game:\/\/[^/]+\/[^/]+$/)) expect(individualGameResources.length).toBeGreaterThan(0) }) @@ -140,8 +137,9 @@ describe('MCP Server Integration', () => { gameType: 'tic-tac-toe' }) - expect(result.gameId).toBe('new-game-id') - expect(result.message).toContain('Created new Tic-Tac-Toe game') + const created = result as any + expect(created.gameId).toBe('new-game-id') + expect(created.message).toContain('Created new Tic-Tac-Toe game') }) it('should handle play moves correctly', async () => { @@ -170,9 +168,10 @@ describe('MCP Server Integration', () => { gameType: 'tic-tac-toe' }) - expect(result.gameId).toBe('test-game') - expect(result.aiMove).toBeDefined() - expect(result.message).toContain('AI made move') + const playResult = result as any + expect(playResult.gameId).toBe('test-game') + expect(playResult.aiMove).toBeDefined() + expect(playResult.message).toContain('AI made move') }) it('should handle invalid tool names', async () => { diff --git a/mcp-server/src/server.ts b/mcp-server/src/server.ts index 399a2c1..7e3b0db 100644 --- a/mcp-server/src/server.ts +++ b/mcp-server/src/server.ts @@ -71,7 +71,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { // conversational flow between moves. try { - const result = await handleToolCall(name, args, server) + const result = await handleToolCall(name, args ?? {}, server) return { content: [ { @@ -94,7 +94,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }) // Start the server -async function main() { +async function main(): Promise { console.error('Starting Turn-based Games MCP server...') const transport = new StdioServerTransport() diff --git a/mcp-server/src/utils/http-client.ts b/mcp-server/src/utils/http-client.ts index 650be33..ecb7cb3 100644 --- a/mcp-server/src/utils/http-client.ts +++ b/mcp-server/src/utils/http-client.ts @@ -7,13 +7,21 @@ import { httpGet, httpPost, WEB_API_BASE } from '@turn-based-mcp/shared' +interface GenericGameStateWrapper { + gameState?: { id?: string; status?: string; currentPlayerId?: string; winner?: string | null; createdAt?: string; updatedAt?: string; [k: string]: unknown } + difficulty?: string + [k: string]: unknown +} +interface CreateGameOptions { [k: string]: unknown } +interface MovePayload { choice?: string; row?: number; col?: number; [k: string]: unknown } + /** * Generic game state fetcher for resources */ -export async function getGameViaAPI(gameType: string, gameId: string) { +export async function getGameViaAPI(gameType: string, gameId: string): Promise { try { const games = await httpGet(`${WEB_API_BASE}/api/games/${gameType}/mcp`) - return games.find((game: any) => game.gameState?.id === gameId) + return (games as GenericGameStateWrapper[]).find((game) => game.gameState?.id === gameId) } catch (error) { console.error(`Error fetching ${gameType} game via API:`, error) return undefined @@ -28,10 +36,10 @@ export async function createGameViaAPI( playerName: string, gameId?: string, difficulty?: string, - gameSpecificOptions?: Record -) { + gameSpecificOptions?: CreateGameOptions +): Promise { try { - const data: any = { playerName } + const data: Record = { playerName } if (gameId) { data.gameId = gameId } @@ -55,9 +63,9 @@ export async function createGameViaAPI( export async function submitMoveViaAPI( gameType: string, gameId: string, - move: any, + move: MovePayload, playerId: string -) { +): Promise { return await httpPost(`${WEB_API_BASE}/api/games/${gameType}/${gameId}/move`, { move, playerId @@ -67,7 +75,7 @@ export async function submitMoveViaAPI( /** * Get all games of a specific type */ -export async function getGamesByType(gameType: string) { +export async function getGamesByType(gameType: string): Promise { return await httpGet(`${WEB_API_BASE}/api/games/${gameType}/mcp`) } diff --git a/mcp-server/vitest.setup.ts b/mcp-server/vitest.setup.ts index 5f8758d..6c5d4a3 100644 --- a/mcp-server/vitest.setup.ts +++ b/mcp-server/vitest.setup.ts @@ -4,7 +4,7 @@ * Uses shared test setup utilities to ensure consistent database setup */ -import { vi } from 'vitest' +// Removed unused 'vi' import (lint) import { setupStandardTestDatabase } from '@turn-based-mcp/shared/dist/testing/vitest-setup.js' // Setup standard test database using shared utility From c3c121394a130b9d379013cf732009d134178f7b Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 19:17:40 +0100 Subject: [PATCH 27/37] Fix linting step --- web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/web/package.json b/web/package.json index c57d0ad..c2fad97 100644 --- a/web/package.json +++ b/web/package.json @@ -2,6 +2,7 @@ "name": "@turn-based-mcp/web", "version": "1.0.0", "private": true, + "type": "module", "scripts": { "dev": "next dev", "build": "next build", From f66a85c1ad4361de196561665eb7bf982bb1e1bb Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 19:24:06 +0100 Subject: [PATCH 28/37] Update types --- mcp-server/src/handlers/game-operations.ts | 9 +++--- mcp-server/src/handlers/resource-handlers.ts | 31 +++++++++++++------- mcp-server/src/handlers/tool-handlers.ts | 10 ++++--- mcp-server/src/server.ts | 4 +-- mcp-server/src/utils/http-client.ts | 23 +++++++++++++-- web/next.config.js | 4 +-- web/postcss.config.js | 4 +-- 7 files changed, 58 insertions(+), 27 deletions(-) diff --git a/mcp-server/src/handlers/game-operations.ts b/mcp-server/src/handlers/game-operations.ts index 6f2096d..9cea2fd 100644 --- a/mcp-server/src/handlers/game-operations.ts +++ b/mcp-server/src/handlers/game-operations.ts @@ -22,11 +22,12 @@ interface GameSessionWrapper { gameState: TicTacToeGameState | RPSGameState; dif export async function readGameResource(gameType: string, gameId: string): Promise { const uri = `game://${gameType}/${gameId}` try { - const gameSession = await getGameViaAPI(gameType, gameId) - if (!gameSession) { + const gameSession = await getGameViaAPI(gameType, gameId) + if (!gameSession || !gameSession.gameState) { throw new Error(`Game not found: ${uri}`) } - return gameSession + // Cast only after verifying presence + return gameSession as unknown as GameSessionWrapper } catch (error) { throw new Error(`Failed to read game resource ${uri}: ${error}`) } @@ -359,7 +360,7 @@ export async function createGame( const gameSession = await createGameViaAPI(gameType, playerName, gameId, difficulty, gameSpecificOptions) const response: Record = { - gameId: gameSession.gameState.id, + gameId: gameSession.gameState.id as string, gameType, gameState: gameSession.gameState, players: gameSession.gameState.players, diff --git a/mcp-server/src/handlers/resource-handlers.ts b/mcp-server/src/handlers/resource-handlers.ts index 389bf39..e3cc2a7 100644 --- a/mcp-server/src/handlers/resource-handlers.ts +++ b/mcp-server/src/handlers/resource-handlers.ts @@ -2,7 +2,7 @@ * MCP Resource handlers for game resources */ -import { getGamesByType, getGameViaAPI } from '../utils/http-client.js' +import { getGamesByType, getGameViaAPI, type GenericGameStateWrapper } from '../utils/http-client.js' import { GAME_TYPES, isSupportedGameType } from '@turn-based-mcp/shared' /** @@ -101,16 +101,25 @@ export async function readResource(uri: string): Promise<{ contents: Array<{ uri mimeType: 'application/json', text: JSON.stringify({ gameType, - games: games.map((game: { gameState?: { id?: string; status?: string; currentPlayerId?: string; winner?: string | null; createdAt?: string; updatedAt?: string; players?: Record }; difficulty?: string }) => ({ - gameId: game.gameState?.id, - status: game.gameState?.status, - currentPlayer: game.gameState?.currentPlayerId, - winner: game.gameState?.winner || null, - createdAt: game.gameState?.createdAt, - updatedAt: game.gameState?.updatedAt, - playerCount: Object.keys(game.gameState?.players || {}).length, - difficulty: game.difficulty - })), + games: games.map((game: GenericGameStateWrapper) => { + const playersUnknown: unknown = (game.gameState as Record).players + let playerCount = 0 + if (Array.isArray(playersUnknown)) { + playerCount = playersUnknown.length + } else if (playersUnknown && typeof playersUnknown === 'object') { + playerCount = Object.keys(playersUnknown as Record).length + } + return { + gameId: game.gameState.id, + status: game.gameState.status, + currentPlayer: game.gameState.currentPlayerId, + winner: game.gameState.winner || null, + createdAt: game.gameState.createdAt, + updatedAt: game.gameState.updatedAt, + playerCount, + difficulty: game.difficulty + } + }), totalGames: games.length, timestamp: new Date().toISOString() }) diff --git a/mcp-server/src/handlers/tool-handlers.ts b/mcp-server/src/handlers/tool-handlers.ts index 2d6c3b7..4252f83 100644 --- a/mcp-server/src/handlers/tool-handlers.ts +++ b/mcp-server/src/handlers/tool-handlers.ts @@ -118,8 +118,10 @@ export const TOOL_DEFINITIONS = [ /** * Handle tool execution */ +// Match the SDK server's elicitInput signature loosely to avoid incompatibility issues. export interface ServerWithElicit { - elicitInput: (args: { message: string; requestedSchema: unknown }) => Promise<{ action: 'accept' | 'decline' | 'cancel'; content?: Record }> + // Allow extra properties on args to accommodate SDK's added metadata fields. + elicitInput: (args: { message: string; requestedSchema: unknown; [k: string]: unknown }) => Promise<{ action: 'accept' | 'decline' | 'cancel'; content?: Record }> } export async function handleToolCall(name: string, args: Record, server?: ServerWithElicit): Promise { @@ -207,11 +209,11 @@ async function createGameWithElicitation(gameType: string, gameId?: string, serv } if (elicitationResult.action === 'accept' && elicitationResult.content) { - const { difficulty, playerName, playerSymbol, maxRounds } = elicitationResult.content + const { difficulty, playerName, playerSymbol, maxRounds } = elicitationResult.content // Prepare game creation parameters - const finalPlayerName = playerName || 'Player' - const finalDifficulty = difficulty || 'medium' + const finalPlayerName = (playerName as string) || 'Player' + const finalDifficulty = (difficulty as string) || 'medium' // Prepare game-specific options const gameSpecificOptions: Record = {} diff --git a/mcp-server/src/server.ts b/mcp-server/src/server.ts index 7e3b0db..500d13d 100644 --- a/mcp-server/src/server.ts +++ b/mcp-server/src/server.ts @@ -13,7 +13,7 @@ import { // Import handlers import { listResources, readResource } from './handlers/resource-handlers.js' -import { TOOL_DEFINITIONS, handleToolCall } from './handlers/tool-handlers.js' +import { TOOL_DEFINITIONS, handleToolCall, type ServerWithElicit } from './handlers/tool-handlers.js' import { listPrompts, getPrompt } from './handlers/prompt-handlers.js' const server = new Server( @@ -71,7 +71,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { // conversational flow between moves. try { - const result = await handleToolCall(name, args ?? {}, server) + const result = await handleToolCall(name, args ?? {}, server as unknown as ServerWithElicit) return { content: [ { diff --git a/mcp-server/src/utils/http-client.ts b/mcp-server/src/utils/http-client.ts index ecb7cb3..70dfdea 100644 --- a/mcp-server/src/utils/http-client.ts +++ b/mcp-server/src/utils/http-client.ts @@ -6,10 +6,29 @@ */ import { httpGet, httpPost, WEB_API_BASE } from '@turn-based-mcp/shared' +import type { GameSession } from '@turn-based-mcp/shared' +import type { TicTacToeGameState, RPSGameState } from '@turn-based-mcp/shared' -interface GenericGameStateWrapper { - gameState?: { id?: string; status?: string; currentPlayerId?: string; winner?: string | null; createdAt?: string; updatedAt?: string; [k: string]: unknown } +// Union of supported game session types the MCP server cares about +export type SupportedGameSession = + | GameSession + | GameSession + +// Narrowed lightweight shape used internally when we just need core fields +type MinimalGameState = { + id: string + status: string + currentPlayerId: string + winner?: string | 'draw' + updatedAt: string | Date + createdAt: string | Date + [k: string]: unknown +} + +export interface GenericGameStateWrapper { + gameState: MinimalGameState & Record difficulty?: string + history?: unknown[] [k: string]: unknown } interface CreateGameOptions { [k: string]: unknown } diff --git a/web/next.config.js b/web/next.config.js index e964b2e..556b3cb 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -4,6 +4,6 @@ const nextConfig = { // Enable experimental features if needed }, transpilePackages: ['@turn-based-mcp/shared'], -} +}; -module.exports = nextConfig +export default nextConfig; diff --git a/web/postcss.config.js b/web/postcss.config.js index 52b9b4b..a34a3d5 100644 --- a/web/postcss.config.js +++ b/web/postcss.config.js @@ -1,5 +1,5 @@ -module.exports = { +export default { plugins: { '@tailwindcss/postcss': {}, }, -} +}; From 05d17467da4904f31241601b84fb0659886358a1 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 19:35:07 +0100 Subject: [PATCH 29/37] Fixing quality checks --- mcp-server/vitest.setup.ts | 3 +-- shared/package.json | 14 ++++++++++++++ shared/src/index.ts | 1 - web/src/components/shared/DifficultyBadge.test.tsx | 3 +-- web/src/components/shared/DifficultyBadge.tsx | 3 +-- web/src/test-utils/api-route-mocks.ts | 7 ++++--- web/vitest.setup.ts | 2 +- 7 files changed, 22 insertions(+), 11 deletions(-) diff --git a/mcp-server/vitest.setup.ts b/mcp-server/vitest.setup.ts index 6c5d4a3..7b3e7a8 100644 --- a/mcp-server/vitest.setup.ts +++ b/mcp-server/vitest.setup.ts @@ -4,8 +4,7 @@ * Uses shared test setup utilities to ensure consistent database setup */ -// Removed unused 'vi' import (lint) -import { setupStandardTestDatabase } from '@turn-based-mcp/shared/dist/testing/vitest-setup.js' +import { setupStandardTestDatabase } from '@turn-based-mcp/shared/testing' // Setup standard test database using shared utility setupStandardTestDatabase() diff --git a/shared/package.json b/shared/package.json index 818ec95..a15dee8 100644 --- a/shared/package.json +++ b/shared/package.json @@ -5,6 +5,20 @@ "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./testing": { + "import": "./dist/testing/index.js", + "types": "./dist/testing/index.d.ts" + }, + "./constants": { + "import": "./dist/constants/index.js", + "types": "./dist/constants/index.d.ts" + } + }, "scripts": { "build": "tsc && fix-esm-import-path dist", "dev": "tsc --watch", diff --git a/shared/src/index.ts b/shared/src/index.ts index 1f6160a..e8873f9 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -4,4 +4,3 @@ export * from './games/index'; export * from './utils/index'; export * from './storage/index'; export * from './constants/index'; -export * from './testing/index'; diff --git a/web/src/components/shared/DifficultyBadge.test.tsx b/web/src/components/shared/DifficultyBadge.test.tsx index 2e00734..19d6452 100644 --- a/web/src/components/shared/DifficultyBadge.test.tsx +++ b/web/src/components/shared/DifficultyBadge.test.tsx @@ -7,8 +7,7 @@ import React from 'react' import { render, screen } from '@testing-library/react' import { DifficultyBadge } from './DifficultyBadge' -// Mock the shared library constants -vi.mock('@turn-based-mcp/shared/dist/constants/game-constants', () => ({ +vi.mock('@turn-based-mcp/shared/constants', () => ({ getDifficultyDisplay: vi.fn((difficulty: string) => { const displays: Record = { easy: { emoji: '😌', label: 'Easy' }, diff --git a/web/src/components/shared/DifficultyBadge.tsx b/web/src/components/shared/DifficultyBadge.tsx index 7eea492..470f85d 100644 --- a/web/src/components/shared/DifficultyBadge.tsx +++ b/web/src/components/shared/DifficultyBadge.tsx @@ -1,7 +1,6 @@ 'use client' -import { getDifficultyDisplay } from '@turn-based-mcp/shared/dist/constants/game-constants' -import type { Difficulty } from '@turn-based-mcp/shared/dist/types/game' +import { getDifficultyDisplay, type Difficulty } from '@turn-based-mcp/shared/constants' /** * Props for the DifficultyBadge component diff --git a/web/src/test-utils/api-route-mocks.ts b/web/src/test-utils/api-route-mocks.ts index 8b9304a..0bc8828 100644 --- a/web/src/test-utils/api-route-mocks.ts +++ b/web/src/test-utils/api-route-mocks.ts @@ -8,13 +8,14 @@ import { vi } from 'vitest' import { NextRequest } from 'next/server' import type { GameType } from '@turn-based-mcp/shared' -import { - createSharedGameMocks, + +import { + createSharedGameMocks, createStorageMocks, createMockTicTacToeGameState, createMockRPSGameState, createMockGameSession -} from '@turn-based-mcp/shared' +} from '@turn-based-mcp/shared/testing' /** * Setup standard API route mocks for a specific game type diff --git a/web/vitest.setup.ts b/web/vitest.setup.ts index c6e3f02..edaf8f1 100644 --- a/web/vitest.setup.ts +++ b/web/vitest.setup.ts @@ -3,7 +3,7 @@ import { vi } from 'vitest' import '@testing-library/jest-dom' // Use shared test database setup -import { setupStandardTestDatabase } from '@turn-based-mcp/shared/dist/testing/vitest-setup.js' +import { setupStandardTestDatabase } from '@turn-based-mcp/shared/testing' // Setup standard test database using shared utility setupStandardTestDatabase() From 868d088b6baa1e009a0a7ebb9997d446562e0ae8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:42:53 +0000 Subject: [PATCH 30/37] Bump the npm-development group with 4 updates Bumps the npm-development group with 4 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [typescript](https://github.com/microsoft/TypeScript), [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) and [undici](https://github.com/nodejs/undici). Updates `@types/node` from 24.1.0 to 24.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `typescript` from 5.8.3 to 5.9.2 - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.2) Updates `eslint-config-next` from 15.4.5 to 15.4.6 - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/commits/v15.4.6/packages/eslint-config-next) Updates `undici` from 7.12.0 to 7.13.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.12.0...v7.13.0) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 24.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-development - dependency-name: typescript dependency-version: 5.9.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-development - dependency-name: eslint-config-next dependency-version: 15.4.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-development - dependency-name: undici dependency-version: 7.13.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-development ... Signed-off-by: dependabot[bot] --- mcp-server/package.json | 4 +-- package-lock.json | 58 ++++++++++++++++++++--------------------- package.json | 4 +-- shared/package.json | 2 +- web/package.json | 8 +++--- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/mcp-server/package.json b/mcp-server/package.json index 275a738..7024e62 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -23,9 +23,9 @@ }, "devDependencies": { "@eslint/js": "^9.32.0", - "@types/node": "^24.1.0", + "@types/node": "^24.2.0", "@vitest/ui": "^3.2.4", - "typescript": "^5.5.0", + "typescript": "^5.9.2", "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" } diff --git a/package-lock.json b/package-lock.json index a74db0c..06f9d48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,10 +18,10 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/eslint__js": "^8.42.3", - "@types/node": "^24.1.0", + "@types/node": "^24.2.0", "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", - "typescript": "^5.5.0", + "typescript": "^5.9.2", "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" }, @@ -38,9 +38,9 @@ }, "devDependencies": { "@eslint/js": "^9.32.0", - "@types/node": "^24.1.0", + "@types/node": "^24.2.0", "@vitest/ui": "^3.2.4", - "typescript": "^5.5.0", + "typescript": "^5.9.2", "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" } @@ -1886,9 +1886,9 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.5.tgz", - "integrity": "sha512-YhbrlbEt0m4jJnXHMY/cCUDBAWgd5SaTa5mJjzOt82QwflAFfW/h3+COp2TfVSzhmscIZ5sg2WXt3MLziqCSCw==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.6.tgz", + "integrity": "sha512-2NOu3ln+BTcpnbIDuxx6MNq+pRrCyey4WSXGaJIyt0D2TYicHeO9QrUENNjcf673n3B1s7hsiV5xBYRCK1Q8kA==", "dev": true, "license": "MIT", "dependencies": { @@ -3096,13 +3096,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/react": { @@ -5580,13 +5580,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.4.5.tgz", - "integrity": "sha512-IMijiXaZ43qFB+Gcpnb374ipTKD8JIyVNR+6VsifFQ/LHyx+A9wgcgSIhCX5PYSjwOoSYD5LtNHKlM5uc23eww==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.4.6.tgz", + "integrity": "sha512-4uznvw5DlTTjrZgYZjMciSdDDMO2SWIuQgUNaFyC2O3Zw3Z91XeIejeVa439yRq2CnJb/KEvE4U2AeN/66FpUA==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.4.5", + "@next/eslint-plugin-next": "15.4.6", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -11360,9 +11360,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -11417,9 +11417,9 @@ } }, "node_modules/undici": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz", - "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.13.0.tgz", + "integrity": "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==", "dev": true, "license": "MIT", "engines": { @@ -11427,9 +11427,9 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, @@ -12192,7 +12192,7 @@ "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", "fix-esm-import-path": "^1.10.3", - "typescript": "^5.5.0", + "typescript": "^5.9.2", "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" } @@ -12216,22 +12216,22 @@ "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.1.0", + "@types/node": "^24.2.0", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", - "eslint-config-next": "^15.4.5", + "eslint-config-next": "^15.4.6", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "jsdom": "^26.1.0", "node-fetch": "^3.3.2", "postcss": "^8.4.40", "tailwindcss": "^4.1.11", - "typescript": "^5.5.0", + "typescript": "^5.9.2", "typescript-eslint": "^8.39.0", - "undici": "^7.12.0", + "undici": "^7.13.0", "vitest": "^3.2.4" } } diff --git a/package.json b/package.json index 74400c5..fad0e98 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,10 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/eslint__js": "^8.42.3", - "@types/node": "^24.1.0", + "@types/node": "^24.2.0", "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", - "typescript": "^5.5.0", + "typescript": "^5.9.2", "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" }, diff --git a/shared/package.json b/shared/package.json index a15dee8..584e097 100644 --- a/shared/package.json +++ b/shared/package.json @@ -39,7 +39,7 @@ "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", "fix-esm-import-path": "^1.10.3", - "typescript": "^5.5.0", + "typescript": "^5.9.2", "typescript-eslint": "^8.39.0", "vitest": "^3.2.4" }, diff --git a/web/package.json b/web/package.json index c2fad97..fa27be5 100644 --- a/web/package.json +++ b/web/package.json @@ -32,22 +32,22 @@ "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.1.0", + "@types/node": "^24.2.0", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", - "eslint-config-next": "^15.4.5", + "eslint-config-next": "^15.4.6", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "jsdom": "^26.1.0", "node-fetch": "^3.3.2", "postcss": "^8.4.40", "tailwindcss": "^4.1.11", - "typescript": "^5.5.0", + "typescript": "^5.9.2", "typescript-eslint": "^8.39.0", - "undici": "^7.12.0", + "undici": "^7.13.0", "vitest": "^3.2.4" } } From 2be1209dca8d849381807bd19297ccbd75f34dc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:43:25 +0000 Subject: [PATCH 31/37] Bump the npm-production group with 2 updates Bumps the npm-production group with 2 updates: [next](https://github.com/vercel/next.js) and [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk). Updates `next` from 15.4.5 to 15.4.6 - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v15.4.5...v15.4.6) Updates `@modelcontextprotocol/sdk` from 1.17.0 to 1.17.1 - [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases) - [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/1.17.0...1.17.1) --- updated-dependencies: - dependency-name: next dependency-version: 15.4.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-production - dependency-name: "@modelcontextprotocol/sdk" dependency-version: 1.17.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-production ... Signed-off-by: dependabot[bot] --- mcp-server/package.json | 2 +- package-lock.json | 88 ++++++++++++++++++++--------------------- web/package.json | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/mcp-server/package.json b/mcp-server/package.json index 275a738..bb18e2b 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -18,7 +18,7 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.1", "@turn-based-mcp/shared": "file:../shared" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index a74db0c..313e08f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "name": "@turn-based-mcp/mcp-server", "version": "1.0.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.1", "@turn-based-mcp/shared": "file:../shared" }, "devDependencies": { @@ -1844,9 +1844,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.0.tgz", - "integrity": "sha512-qFfbWFA7r1Sd8D697L7GkTd36yqDuTkvz0KfOGkgXR8EUhQn3/EDNIR/qUdQNMT8IjmasBvHWuXeisxtXTQT2g==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.1.tgz", + "integrity": "sha512-CPle1OQehbWqd25La9Ack5B07StKIxh4+Bf19qnpZKJC1oI22Y0czZHbifjw1UoczIfKBwBDAp/dFxvHG13B5A==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -1880,9 +1880,9 @@ } }, "node_modules/@next/env": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.5.tgz", - "integrity": "sha512-ruM+q2SCOVCepUiERoxOmZY9ZVoecR3gcXNwCYZRvQQWRjhOiPJGmQ2fAiLR6YKWXcSAh7G79KEFxN3rwhs4LQ==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.6.tgz", + "integrity": "sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1896,9 +1896,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.5.tgz", - "integrity": "sha512-84dAN4fkfdC7nX6udDLz9GzQlMUwEMKD7zsseXrl7FTeIItF8vpk1lhLEnsotiiDt+QFu3O1FVWnqwcRD2U3KA==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz", + "integrity": "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==", "cpu": [ "arm64" ], @@ -1912,9 +1912,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.5.tgz", - "integrity": "sha512-CL6mfGsKuFSyQjx36p2ftwMNSb8PQog8y0HO/ONLdQqDql7x3aJb/wB+LA651r4we2pp/Ck+qoRVUeZZEvSurA==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz", + "integrity": "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==", "cpu": [ "x64" ], @@ -1928,9 +1928,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.5.tgz", - "integrity": "sha512-1hTVd9n6jpM/thnDc5kYHD1OjjWYpUJrJxY4DlEacT7L5SEOXIifIdTye6SQNNn8JDZrcN+n8AWOmeJ8u3KlvQ==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz", + "integrity": "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==", "cpu": [ "arm64" ], @@ -1944,9 +1944,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.5.tgz", - "integrity": "sha512-4W+D/nw3RpIwGrqpFi7greZ0hjrCaioGErI7XHgkcTeWdZd146NNu1s4HnaHonLeNTguKnL2Urqvj28UJj6Gqw==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz", + "integrity": "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==", "cpu": [ "arm64" ], @@ -1960,9 +1960,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.5.tgz", - "integrity": "sha512-N6Mgdxe/Cn2K1yMHge6pclffkxzbSGOydXVKYOjYqQXZYjLCfN/CuFkaYDeDHY2VBwSHyM2fUjYBiQCIlxIKDA==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz", + "integrity": "sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==", "cpu": [ "x64" ], @@ -1976,9 +1976,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.5.tgz", - "integrity": "sha512-YZ3bNDrS8v5KiqgWE0xZQgtXgCTUacgFtnEgI4ccotAASwSvcMPDLua7BWLuTfucoRv6mPidXkITJLd8IdJplQ==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.6.tgz", + "integrity": "sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==", "cpu": [ "x64" ], @@ -1992,9 +1992,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.5.tgz", - "integrity": "sha512-9Wr4t9GkZmMNcTVvSloFtjzbH4vtT4a8+UHqDoVnxA5QyfWe6c5flTH1BIWPGNWSUlofc8dVJAE7j84FQgskvQ==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz", + "integrity": "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==", "cpu": [ "arm64" ], @@ -2008,9 +2008,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.5.tgz", - "integrity": "sha512-voWk7XtGvlsP+w8VBz7lqp8Y+dYw/MTI4KeS0gTVtfdhdJ5QwhXLmNrndFOin/MDoCvUaLWMkYKATaCoUkt2/A==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz", + "integrity": "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==", "cpu": [ "x64" ], @@ -8790,12 +8790,12 @@ } }, "node_modules/next": { - "version": "15.4.5", - "resolved": "https://registry.npmjs.org/next/-/next-15.4.5.tgz", - "integrity": "sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g==", + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/next/-/next-15.4.6.tgz", + "integrity": "sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==", "license": "MIT", "dependencies": { - "@next/env": "15.4.5", + "@next/env": "15.4.6", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -8808,14 +8808,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.4.5", - "@next/swc-darwin-x64": "15.4.5", - "@next/swc-linux-arm64-gnu": "15.4.5", - "@next/swc-linux-arm64-musl": "15.4.5", - "@next/swc-linux-x64-gnu": "15.4.5", - "@next/swc-linux-x64-musl": "15.4.5", - "@next/swc-win32-arm64-msvc": "15.4.5", - "@next/swc-win32-x64-msvc": "15.4.5", + "@next/swc-darwin-arm64": "15.4.6", + "@next/swc-darwin-x64": "15.4.6", + "@next/swc-linux-arm64-gnu": "15.4.6", + "@next/swc-linux-arm64-musl": "15.4.6", + "@next/swc-linux-x64-gnu": "15.4.6", + "@next/swc-linux-x64-musl": "15.4.6", + "@next/swc-win32-arm64-msvc": "15.4.6", + "@next/swc-win32-x64-msvc": "15.4.6", "sharp": "^0.34.3" }, "peerDependencies": { @@ -12204,7 +12204,7 @@ "@turn-based-mcp/shared": "file:../shared", "@types/sqlite3": "^5.1.0", "clsx": "^2.1.0", - "next": "^15.4.5", + "next": "^15.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", "sqlite3": "^5.1.7", diff --git a/web/package.json b/web/package.json index c2fad97..0941002 100644 --- a/web/package.json +++ b/web/package.json @@ -20,7 +20,7 @@ "@turn-based-mcp/shared": "file:../shared", "@types/sqlite3": "^5.1.0", "clsx": "^2.1.0", - "next": "^15.4.5", + "next": "^15.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", "sqlite3": "^5.1.7", From febb9838ae1187fc8ff73fe22effcd42baf8c0bc Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:41:16 +0100 Subject: [PATCH 32/37] Update deps, CI and configs --- .github/workflows/ci.yml | 32 +- mcp-server/tsconfig.json | 2 +- package-lock.json | 1374 +++++++++++++++++--------------------- package.json | 4 +- shared/package.json | 12 +- shared/tsconfig.json | 2 +- 6 files changed, 634 insertions(+), 792 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75a5c72..6ab9cea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,26 +31,30 @@ jobs: run: npm ci - name: Build shared package - run: npm run build --workspace=shared + run: npm run build --workspace=@turn-based-mcp/shared + + - name: Verify shared build artifacts + run: | + test -f shared/dist/index.js || (echo 'shared/dist/index.js missing' && exit 1) + test -f shared/dist/testing/index.js || (echo 'shared/dist/testing/index.js missing' && exit 1) - name: Type check all workspaces run: npm run type-check - name: Build web package - run: npm run build --workspace=web + run: npm run build --workspace=@turn-based-mcp/web - name: Build mcp-server package - run: npm run build --workspace=mcp-server + run: npm run build --workspace=@turn-based-mcp/mcp-server + + - name: Run tests - shared + run: npm run test --workspace=@turn-based-mcp/shared + + - name: Run tests - web + run: npm run test --workspace=@turn-based-mcp/web + + - name: Run tests - mcp-server + run: npm run test --workspace=@turn-based-mcp/mcp-server - name: Lint web package - run: npm run lint --workspace=web - - # Future: Add test steps when tests are implemented - # - name: Run tests - shared - # run: npm run test --workspace=shared - # - # - name: Run tests - web - # run: npm run test --workspace=web - # - # - name: Run tests - mcp-server - # run: npm run test --workspace=mcp-server \ No newline at end of file + run: npm run lint --workspace=@turn-based-mcp/web \ No newline at end of file diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json index 031bbe4..343f88c 100644 --- a/mcp-server/tsconfig.json +++ b/mcp-server/tsconfig.json @@ -12,7 +12,7 @@ "strict": true, "skipLibCheck": true, "esModuleInterop": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true }, "include": ["src/**/*"], "exclude": ["dist", "node_modules", "**/*.test.ts", "**/*.test.tsx"] diff --git a/package-lock.json b/package-lock.json index 3aae736..bc26102 100644 --- a/package-lock.json +++ b/package-lock.json @@ -156,19 +156,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -550,33 +537,6 @@ "node": ">=18" } }, - "node_modules/@edge-runtime/primitives": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@edge-runtime/primitives/-/primitives-6.0.0.tgz", - "integrity": "sha512-FqoxaBT+prPBHBwE1WXS1ocnu/VLTQyZ6NMUBAdbP7N2hsFTTxMC/jMu2D/8GAlMQfxeuppcPuCUk/HO3fpIvA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@edge-runtime/vm": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@edge-runtime/vm/-/vm-5.0.0.tgz", - "integrity": "sha512-NKBGBSIKUG584qrS1tyxVpX/AKJKQw5HgjYEnPLC0QsTw79JrGn+qUr8CXFb955Iy7GUdiiUv1rJ6JBGvaKb6w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@edge-runtime/primitives": "6.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@emnapi/core": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", @@ -1702,6 +1662,53 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1715,6 +1722,16 @@ "node": ">=18.0.0" } }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1895,6 +1912,36 @@ "fast-glob": "3.3.1" } }, + "node_modules/@next/eslint-plugin-next/node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@next/swc-darwin-arm64": { "version": "15.4.6", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz", @@ -2694,73 +2741,6 @@ "node": ">= 10" } }, - "node_modules/@tailwindcss/oxide/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/@tailwindcss/postcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", @@ -2776,9 +2756,9 @@ } }, "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", "peer": true, @@ -2787,26 +2767,15 @@ "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", - "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", + "picocolors": "1.1.1", "pretty-format": "^27.0.2" }, "engines": { "node": ">=18" } }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz", @@ -2954,13 +2923,13 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/chai": { @@ -3359,36 +3328,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -3928,15 +3867,6 @@ "node": ">= 0.6" } }, - "node_modules/accepts/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3961,16 +3891,13 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/agentkeepalive": { @@ -4072,13 +3999,13 @@ "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" + "dependencies": { + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -4520,54 +4447,54 @@ "node": ">= 10" } }, - "node_modules/cacache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "license": "ISC", "optional": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "yallist": "^4.0.0" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/cacache/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "license": "ISC", "optional": true, "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", "yallist": "^4.0.0" }, "engines": { "node": ">=10" } }, - "node_modules/cacache/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/cacache/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "license": "ISC", "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, "engines": { "node": ">=8" } }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4627,9 +4554,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "funding": [ { "type": "opencollective", @@ -5118,7 +5045,6 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -5181,9 +5107,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.190", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", - "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==", + "version": "1.5.198", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.198.tgz", + "integrity": "sha512-G5COfnp3w+ydVu80yprgWSfmfQaYRh9DOxfhAxstLyetKaLyl55QrNjx8C38Pc/C+RaDmb1M0Lk8wPEMQ+bGgQ==", "dev": true, "license": "ISC" }, @@ -5223,9 +5149,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -5776,6 +5702,16 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -6085,9 +6021,9 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -6095,7 +6031,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -6137,6 +6073,21 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -6301,6 +6252,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -6350,18 +6314,6 @@ "node": ">= 8" } }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6445,48 +6397,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/gauge/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true - }, - "node_modules/gauge/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "optional": true - }, - "node_modules/gauge/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -6572,21 +6482,22 @@ "license": "MIT" }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", + "optional": true, "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6605,32 +6516,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -6829,33 +6714,41 @@ "node": ">= 0.8" } }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/humanize-ms": { @@ -7756,19 +7649,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jiti": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", @@ -7846,63 +7726,25 @@ } } }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, "engines": { - "node": ">= 14" + "node": ">=6" } }, - "node_modules/jsdom/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -7918,16 +7760,16 @@ "license": "MIT" }, "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, "bin": { "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/jsx-ast-utils": { @@ -8299,13 +8141,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lru-cache/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -8383,6 +8218,48 @@ "node": ">= 10" } }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/make-fetch-happen/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -8396,19 +8273,23 @@ "node": ">=10" } }, - "node_modules/make-fetch-happen/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">= 0.6" } }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8463,6 +8344,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -8529,13 +8423,15 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=8" } }, "node_modules/minipass-collect": { @@ -8551,19 +8447,6 @@ "node": ">= 8" } }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/minipass-fetch": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", @@ -8582,19 +8465,6 @@ "encoding": "^0.1.12" } }, - "node_modules/minipass-fetch/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/minipass-flush": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", @@ -8608,19 +8478,6 @@ "node": ">= 8" } }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", @@ -8634,19 +8491,6 @@ "node": ">=8" } }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/minipass-sized": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", @@ -8660,18 +8504,11 @@ "node": ">=8" } }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" }, "node_modules/minizlib": { "version": "2.1.2", @@ -8686,17 +8523,11 @@ "node": ">= 8" } }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" }, "node_modules/mkdirp": { "version": "1.0.4", @@ -8780,11 +8611,10 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.6" } @@ -8952,28 +8782,41 @@ "node": ">= 10.12.0" } }, - "node_modules/node-gyp/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/node-gyp/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "license": "ISC", "optional": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -9015,9 +8858,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", "dev": true, "license": "MIT" }, @@ -9349,7 +9192,17 @@ "dev": true, "license": "ISC" }, - "node_modules/path-to-regexp": { + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", @@ -9382,13 +9235,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -9508,14 +9361,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -9549,6 +9394,13 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9687,11 +9539,12 @@ } }, "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-refresh": { "version": "0.17.0", @@ -9854,28 +9707,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.46.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", @@ -10318,17 +10149,11 @@ "license": "ISC" }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "optional": true }, "node_modules/simple-concat": { "version": "1.0.1", @@ -10451,6 +10276,19 @@ "node": ">= 10" } }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -10491,30 +10329,49 @@ } } }, - "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "node_modules/sqlite3/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/sqlite3/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "license": "ISC", - "optional": true, "dependencies": { - "minipass": "^3.1.1" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" }, "engines": { - "node": ">= 8" + "node": ">=10" } }, - "node_modules/ssri/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/sqlite3/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", "license": "ISC", "optional": true, "dependencies": { - "yallist": "^4.0.0" + "minipass": "^3.1.1" }, "engines": { - "node": ">=8" + "node": ">= 8" } }, "node_modules/stable-hash": { @@ -10555,9 +10412,9 @@ "license": "MIT" }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -10594,21 +10451,18 @@ } }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/string-width-cjs": { @@ -10634,18 +10488,12 @@ "dev": true, "license": "MIT" }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true, + "license": "MIT" }, "node_modules/string.prototype.includes": { "version": "2.0.1", @@ -10761,19 +10609,16 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/strip-ansi-cjs": { @@ -10790,19 +10635,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -10943,20 +10775,21 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, "license": "ISC", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-fs": { @@ -10993,13 +10826,63 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "license": "ISC", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" } }, "node_modules/test-exclude": { @@ -11027,6 +10910,27 @@ "balanced-match": "^1.0.0" } }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/test-exclude/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -11043,6 +10947,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/test-exclude/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -11074,34 +10988,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -11236,6 +11122,19 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -11650,34 +11549,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vitest": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", @@ -11751,19 +11622,6 @@ } } }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -11965,41 +11823,6 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "node_modules/wide-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true - }, - "node_modules/wide-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -12047,52 +11870,64 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/wrappy": { @@ -12141,9 +11976,10 @@ "license": "MIT" }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yocto-queue": { diff --git a/package.json b/package.json index fad0e98..88ff81a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "test:coverage": "npm run test:coverage --workspace=shared && npm run test:coverage --workspace=web && npm run test:coverage --workspace=mcp-server", "lint": "eslint .", "lint:fix": "eslint . --fix", - "lint:all": "npm run lint --workspace=shared && npm run lint --workspace=web && npm run lint --workspace=mcp-server" + "lint:all": "npm run lint --workspace=shared && npm run lint --workspace=web && npm run lint --workspace=mcp-server", + "ci:tests": "npm run test --workspace=@turn-based-mcp/shared && npm run test --workspace=@turn-based-mcp/web && npm run test --workspace=@turn-based-mcp/mcp-server", + "ci": "npm run build --workspace=@turn-based-mcp/shared && npm run type-check && npm run build --workspace=@turn-based-mcp/web && npm run build --workspace=@turn-based-mcp/mcp-server && npm run ci:tests && npm run lint --workspace=@turn-based-mcp/web" }, "devDependencies": { "@eslint/js": "^9.32.0", diff --git a/shared/package.json b/shared/package.json index 584e097..472e699 100644 --- a/shared/package.json +++ b/shared/package.json @@ -7,16 +7,16 @@ "types": "dist/index.d.ts", "exports": { ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "import": "./dist/index.js", + "types": "./dist/index.d.ts" }, "./testing": { - "import": "./dist/testing/index.js", - "types": "./dist/testing/index.d.ts" + "import": "./dist/testing/index.js", + "types": "./dist/testing/index.d.ts" }, "./constants": { - "import": "./dist/constants/index.js", - "types": "./dist/constants/index.d.ts" + "import": "./dist/constants/index.js", + "types": "./dist/constants/index.d.ts" } }, "scripts": { diff --git a/shared/tsconfig.json b/shared/tsconfig.json index a1b7bdd..517001e 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -15,5 +15,5 @@ "skipLibCheck": true }, "include": ["src/**/*"], - "exclude": ["dist", "node_modules", "**/*.test.ts", "**/*.test.tsx", "src/testing/**/*"] + "exclude": ["dist", "node_modules", "**/*.test.ts", "**/*.test.tsx"] } From 3e32b7dac9ea09cf6602e9fd69b73de6ff521a0a Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:43:49 +0100 Subject: [PATCH 33/37] Replace lint web with lint all --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ab9cea..2854e90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,5 +56,5 @@ jobs: - name: Run tests - mcp-server run: npm run test --workspace=@turn-based-mcp/mcp-server - - name: Lint web package - run: npm run lint --workspace=@turn-based-mcp/web \ No newline at end of file + - name: Lint all packages + run: npm run lint:all \ No newline at end of file From 9bce68fed6f0bc4e573076c977bdb58592b4ace2 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:55:50 +0100 Subject: [PATCH 34/37] Update tool call descriptions --- mcp-server/src/handlers/tool-handlers.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mcp-server/src/handlers/tool-handlers.ts b/mcp-server/src/handlers/tool-handlers.ts index 4252f83..9e84568 100644 --- a/mcp-server/src/handlers/tool-handlers.ts +++ b/mcp-server/src/handlers/tool-handlers.ts @@ -76,7 +76,7 @@ export const TOOL_DEFINITIONS = [ }, { name: 'create_game', - description: 'Create a new game with interactive setup. This will ask you for preferences like difficulty, player options, and other game-specific settings.', + description: 'Create a new game with interactive setup. This will ask you for preferences like difficulty, player options, and other game-specific settings. IMPORTANT: Only provide parameters that the user explicitly specified. DO NOT provide default values for optional parameters - missing parameters will trigger interactive elicitation.', inputSchema: { type: 'object', properties: { @@ -96,12 +96,12 @@ export const TOOL_DEFINITIONS = [ }, playerName: { type: 'string', - description: 'Your name in the game. If not provided, will be asked during setup.' + description: 'Your name in the game. LEAVE EMPTY to trigger interactive setup - do not provide defaults like "Player" or "User".' }, playerSymbol: { type: 'string', enum: ['X', 'O'], - description: 'For tic-tac-toe: your symbol (X goes first, O goes second). If not provided, will be asked during setup.' + description: 'For tic-tac-toe: your symbol (X goes first, O goes second). LEAVE EMPTY to trigger interactive setup - do not auto-select X.' }, maxRounds: { type: 'number', @@ -172,7 +172,7 @@ export async function handleToolCall(name: string, args: Record if (!isSupportedGameType(genericGameType)) { throw new Error(`Unsupported game type: ${genericGameType}`) } - return await createGameWithElicitation(genericGameType, genericGameId, server, args) + return await createGameWithElicitation(genericGameType, genericGameId, server, args) } default: throw new Error(`Unknown tool: ${name}`) From 56b8c713be24cc300e7acfdd6e5dce1ea777bd78 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Fri, 8 Aug 2025 08:24:41 +0100 Subject: [PATCH 35/37] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mcp-server/src/handlers/tool-handlers.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/mcp-server/src/handlers/tool-handlers.ts b/mcp-server/src/handlers/tool-handlers.ts index 9e84568..33b5899 100644 --- a/mcp-server/src/handlers/tool-handlers.ts +++ b/mcp-server/src/handlers/tool-handlers.ts @@ -196,7 +196,23 @@ async function createGameWithElicitation(gameType: string, gameId?: string, serv difficulty: toolArgs?.difficulty, playerSymbol: toolArgs?.playerSymbol, maxRounds: toolArgs?.maxRounds - }) + // Validate toolArgs properties before passing them + const elicitationOptions: Record = { gameId }; + if (toolArgs) { + if (typeof toolArgs.playerName === 'string') { + elicitationOptions.playerName = toolArgs.playerName; + } + if (typeof toolArgs.difficulty === 'string') { + elicitationOptions.difficulty = toolArgs.difficulty; + } + if (typeof toolArgs.playerSymbol === 'string') { + elicitationOptions.playerSymbol = toolArgs.playerSymbol; + } + if (typeof toolArgs.maxRounds === 'number') { + elicitationOptions.maxRounds = toolArgs.maxRounds; + } + } + const elicitationResult = await elicitGameCreationPreferences(server, gameType, elicitationOptions) if (elicitationResult.action === 'decline' || elicitationResult.action === 'cancel') { return { From c7be6db87a99a4c4dfde65264ec4516cfdc4059c Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Fri, 8 Aug 2025 08:34:19 +0100 Subject: [PATCH 36/37] Minor tweaks to init --- mcp-server/src/handlers/tool-handlers.ts | 7 ------- web/src/app/api/games/tic-tac-toe/route.ts | 13 +++---------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/mcp-server/src/handlers/tool-handlers.ts b/mcp-server/src/handlers/tool-handlers.ts index 33b5899..b397eaa 100644 --- a/mcp-server/src/handlers/tool-handlers.ts +++ b/mcp-server/src/handlers/tool-handlers.ts @@ -189,13 +189,6 @@ async function createGameWithElicitation(gameType: string, gameId?: string, serv } try { - // Elicit user preferences - const elicitationResult = await elicitGameCreationPreferences(server, gameType, { - gameId, - playerName: toolArgs?.playerName, - difficulty: toolArgs?.difficulty, - playerSymbol: toolArgs?.playerSymbol, - maxRounds: toolArgs?.maxRounds // Validate toolArgs properties before passing them const elicitationOptions: Record = { gameId }; if (toolArgs) { diff --git a/web/src/app/api/games/tic-tac-toe/route.ts b/web/src/app/api/games/tic-tac-toe/route.ts index ae2c87c..2c0e79f 100644 --- a/web/src/app/api/games/tic-tac-toe/route.ts +++ b/web/src/app/api/games/tic-tac-toe/route.ts @@ -15,16 +15,9 @@ export async function POST(request: NextRequest) { { id: 'ai', name: 'AI', isAI: true } ] - // Determine who goes first based on symbol choice - // X always goes first, O goes second - let options: { firstPlayerId?: string } | undefined; - if (playerSymbol === 'O') { - // Player chose O, so AI (who gets X) goes first - options = { firstPlayerId: 'ai' }; - } else { - // Player chose X (default) or no preference, so player goes first - options = { firstPlayerId: 'player1' }; - } + const options: { firstPlayerId: string } = { + firstPlayerId: playerSymbol === 'O' ? 'ai' : 'player1' + }; const gameState = ticTacToeGame.getInitialState(players, options) From 65a7cac6b8d8d51f599cbd0a78a80a02f082b9aa Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Fri, 8 Aug 2025 08:52:41 +0100 Subject: [PATCH 37/37] Package cleanup --- package-lock.json | 22 ---------------------- package.json | 1 - 2 files changed, 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc26102..19ff948 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/eslint__js": "^8.42.3", "@types/node": "^24.2.0", "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", @@ -2949,27 +2948,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint__js": { - "version": "8.42.3", - "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", - "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", diff --git a/package.json b/package.json index 88ff81a..eb46081 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/eslint__js": "^8.42.3", "@types/node": "^24.2.0", "@vitest/ui": "^3.2.4", "eslint": "^9.32.0",