#!/usr/bin/env node const fs = require('node:fs') const path = require('node:path') const BASE_URL = 'https://loganz2.cn' const CONFIG_NAMES = ['game-sdk.config.json', 'game.config.json'] function fail(message) { console.error(`Error: ${message}`) process.exit(1) } function printUsage() { console.log(`game-sdk CLI Usage: game-sdk publish --game-id --name --bundle [--description ] [--controls ] game-sdk asset --game-id --file [--name ] game-sdk delete --game-id Config lookup order: 1. CLI flags 2. ./game-sdk.config.json 3. ./game.config.json 4. package.json#gameSdk Examples: game-sdk publish --game-id reaction-click --name "反应力挑战" --bundle ./bundle.js --description "快速点击目标拿分" --controls "鼠标点击" game-sdk asset --game-id reaction-click --file ./player.png game-sdk delete --game-id reaction-click `) } function parseArgs(argv) { const args = {} for (let i = 0; i < argv.length; i++) { const token = argv[i] if (!token.startsWith('--')) { fail(`unexpected argument: ${token}`) } const key = token.slice(2) const value = argv[i + 1] if (!value || value.startsWith('--')) { fail(`missing value for --${key}`) } args[key] = value i += 1 } return args } function requireArg(args, key) { const value = args[key] if (!value) fail(`missing required argument --${key}`) return value } function readJsonFile(filePath) { try { return JSON.parse(fs.readFileSync(filePath, 'utf8')) } catch (error) { fail(`invalid JSON in ${filePath}: ${error instanceof Error ? error.message : String(error)}`) } } function loadConfig() { for (const configName of CONFIG_NAMES) { const configPath = path.resolve(process.cwd(), configName) if (fs.existsSync(configPath)) { return readJsonFile(configPath) } } const packageJsonPath = path.resolve(process.cwd(), 'package.json') if (!fs.existsSync(packageJsonPath)) { return {} } const packageJson = readJsonFile(packageJsonPath) return packageJson.gameSdk || {} } function mergeArgs(config, cliArgs) { return { ...config, ...cliArgs } } function assertGameId(gameId) { if (!/^[A-Za-z0-9_-]+$/.test(gameId)) { fail('game id must match /^[A-Za-z0-9_-]+$/') } } function readFile(filePath) { const absolutePath = path.resolve(filePath) if (!fs.existsSync(absolutePath)) { fail(`file not found: ${absolutePath}`) } return { absolutePath, content: fs.readFileSync(absolutePath), } } async function request(url, options) { const response = await fetch(url, options) const text = await response.text() if (!response.ok) { fail(`${response.status} ${response.statusText}${text ? `\n${text}` : ''}`) } if (!text) return null try { return JSON.parse(text) } catch { return text } } async function publish(args) { const gameId = requireArg(args, 'game-id') const name = requireArg(args, 'name') const bundle = requireArg(args, 'bundle') const description = args.description || '' const controls = args.controls || '' assertGameId(gameId) const { absolutePath, content } = readFile(bundle) const result = await request(`${BASE_URL}/api/games/bundle`, { method: 'POST', headers: { 'x-game-id': gameId, 'x-game-name': encodeURIComponent(name), 'x-game-description': encodeURIComponent(description), 'x-game-controls': encodeURIComponent(controls), 'content-type': 'application/javascript', }, body: content, }) console.log(`Uploaded bundle: ${absolutePath}`) console.log(`${BASE_URL}/games/${gameId}`) if (result) { console.log(JSON.stringify(result, null, 2)) } } async function uploadAsset(args) { const gameId = requireArg(args, 'game-id') const filePath = requireArg(args, 'file') const { absolutePath, content } = readFile(filePath) const filename = args.name || path.basename(absolutePath) assertGameId(gameId) const result = await request(`${BASE_URL}/api/games/asset`, { method: 'POST', headers: { 'x-game-id': gameId, 'x-filename': filename, }, body: content, }) console.log(`Uploaded asset: ${absolutePath}`) console.log(`/api/games/${gameId}/assets/${filename}`) if (result) { console.log(JSON.stringify(result, null, 2)) } } async function removeGame(args) { const gameId = requireArg(args, 'game-id') assertGameId(gameId) const result = await request(`${BASE_URL}/api/games/${gameId}`, { method: 'DELETE', }) console.log(`Deleted game: ${gameId}`) if (result) { console.log(JSON.stringify(result, null, 2)) } } async function main() { const [command, ...rest] = process.argv.slice(2) if (!command || command === '--help' || command === '-h') { printUsage() return } const args = mergeArgs(loadConfig(), parseArgs(rest)) if (command === 'publish') { await publish(args) return } if (command === 'asset') { await uploadAsset(args) return } if (command === 'delete') { await removeGame(args) return } fail(`unknown command: ${command}`) } main().catch((error) => { fail(error instanceof Error ? error.message : String(error)) })