cli.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. #!/usr/bin/env node
  2. const fs = require('node:fs')
  3. const path = require('node:path')
  4. const BASE_URL = 'https://loganz2.cn'
  5. const CONFIG_NAMES = ['game-sdk.config.json', 'game.config.json']
  6. function fail(message) {
  7. console.error(`Error: ${message}`)
  8. process.exit(1)
  9. }
  10. function printUsage() {
  11. console.log(`game-sdk CLI
  12. Usage:
  13. game-sdk publish --game-id <id> --name <name> --bundle <file> [--description <text>] [--controls <text>]
  14. game-sdk asset --game-id <id> --file <file> [--name <filename>]
  15. game-sdk delete --game-id <id>
  16. Config lookup order:
  17. 1. CLI flags
  18. 2. ./game-sdk.config.json
  19. 3. ./game.config.json
  20. 4. package.json#gameSdk
  21. Examples:
  22. game-sdk publish --game-id reaction-click --name "反应力挑战" --bundle ./bundle.js --description "快速点击目标拿分" --controls "鼠标点击"
  23. game-sdk asset --game-id reaction-click --file ./player.png
  24. game-sdk delete --game-id reaction-click
  25. `)
  26. }
  27. function parseArgs(argv) {
  28. const args = {}
  29. for (let i = 0; i < argv.length; i++) {
  30. const token = argv[i]
  31. if (!token.startsWith('--')) {
  32. fail(`unexpected argument: ${token}`)
  33. }
  34. const key = token.slice(2)
  35. const value = argv[i + 1]
  36. if (!value || value.startsWith('--')) {
  37. fail(`missing value for --${key}`)
  38. }
  39. args[key] = value
  40. i += 1
  41. }
  42. return args
  43. }
  44. function requireArg(args, key) {
  45. const value = args[key]
  46. if (!value) fail(`missing required argument --${key}`)
  47. return value
  48. }
  49. function readJsonFile(filePath) {
  50. try {
  51. return JSON.parse(fs.readFileSync(filePath, 'utf8'))
  52. } catch (error) {
  53. fail(`invalid JSON in ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
  54. }
  55. }
  56. function loadConfig() {
  57. for (const configName of CONFIG_NAMES) {
  58. const configPath = path.resolve(process.cwd(), configName)
  59. if (fs.existsSync(configPath)) {
  60. return readJsonFile(configPath)
  61. }
  62. }
  63. const packageJsonPath = path.resolve(process.cwd(), 'package.json')
  64. if (!fs.existsSync(packageJsonPath)) {
  65. return {}
  66. }
  67. const packageJson = readJsonFile(packageJsonPath)
  68. return packageJson.gameSdk || {}
  69. }
  70. function mergeArgs(config, cliArgs) {
  71. return { ...config, ...cliArgs }
  72. }
  73. function assertGameId(gameId) {
  74. if (!/^[A-Za-z0-9_-]+$/.test(gameId)) {
  75. fail('game id must match /^[A-Za-z0-9_-]+$/')
  76. }
  77. }
  78. function readFile(filePath) {
  79. const absolutePath = path.resolve(filePath)
  80. if (!fs.existsSync(absolutePath)) {
  81. fail(`file not found: ${absolutePath}`)
  82. }
  83. return {
  84. absolutePath,
  85. content: fs.readFileSync(absolutePath),
  86. }
  87. }
  88. async function request(url, options) {
  89. const response = await fetch(url, options)
  90. const text = await response.text()
  91. if (!response.ok) {
  92. fail(`${response.status} ${response.statusText}${text ? `\n${text}` : ''}`)
  93. }
  94. if (!text) return null
  95. try {
  96. return JSON.parse(text)
  97. } catch {
  98. return text
  99. }
  100. }
  101. async function publish(args) {
  102. const gameId = requireArg(args, 'game-id')
  103. const name = requireArg(args, 'name')
  104. const bundle = requireArg(args, 'bundle')
  105. const description = args.description || ''
  106. const controls = args.controls || ''
  107. assertGameId(gameId)
  108. const { absolutePath, content } = readFile(bundle)
  109. const result = await request(`${BASE_URL}/api/games/bundle`, {
  110. method: 'POST',
  111. headers: {
  112. 'x-game-id': gameId,
  113. 'x-game-name': encodeURIComponent(name),
  114. 'x-game-description': encodeURIComponent(description),
  115. 'x-game-controls': encodeURIComponent(controls),
  116. 'content-type': 'application/javascript',
  117. },
  118. body: content,
  119. })
  120. console.log(`Uploaded bundle: ${absolutePath}`)
  121. console.log(`${BASE_URL}/games/${gameId}`)
  122. if (result) {
  123. console.log(JSON.stringify(result, null, 2))
  124. }
  125. }
  126. async function uploadAsset(args) {
  127. const gameId = requireArg(args, 'game-id')
  128. const filePath = requireArg(args, 'file')
  129. const { absolutePath, content } = readFile(filePath)
  130. const filename = args.name || path.basename(absolutePath)
  131. assertGameId(gameId)
  132. const result = await request(`${BASE_URL}/api/games/asset`, {
  133. method: 'POST',
  134. headers: {
  135. 'x-game-id': gameId,
  136. 'x-filename': filename,
  137. },
  138. body: content,
  139. })
  140. console.log(`Uploaded asset: ${absolutePath}`)
  141. console.log(`/api/games/${gameId}/assets/${filename}`)
  142. if (result) {
  143. console.log(JSON.stringify(result, null, 2))
  144. }
  145. }
  146. async function removeGame(args) {
  147. const gameId = requireArg(args, 'game-id')
  148. assertGameId(gameId)
  149. const result = await request(`${BASE_URL}/api/games/${gameId}`, {
  150. method: 'DELETE',
  151. })
  152. console.log(`Deleted game: ${gameId}`)
  153. if (result) {
  154. console.log(JSON.stringify(result, null, 2))
  155. }
  156. }
  157. async function main() {
  158. const [command, ...rest] = process.argv.slice(2)
  159. if (!command || command === '--help' || command === '-h') {
  160. printUsage()
  161. return
  162. }
  163. const args = mergeArgs(loadConfig(), parseArgs(rest))
  164. if (command === 'publish') {
  165. await publish(args)
  166. return
  167. }
  168. if (command === 'asset') {
  169. await uploadAsset(args)
  170. return
  171. }
  172. if (command === 'delete') {
  173. await removeGame(args)
  174. return
  175. }
  176. fail(`unknown command: ${command}`)
  177. }
  178. main().catch((error) => {
  179. fail(error instanceof Error ? error.message : String(error))
  180. })