| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- #!/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 <id> --name <name> --bundle <file> [--description <text>] [--controls <text>]
- game-sdk asset --game-id <id> --file <file> [--name <filename>]
- game-sdk delete --game-id <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))
- })
|