pogl-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/package.json +25 -0
- package/src/cli.mjs +140 -0
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# pogl
|
|
2
|
+
|
|
3
|
+
Pogl에 웹게임을 배포하는 CLI — Vercel처럼.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx pogl login # 브라우저가 열리고, 승인 한 번으로 연동
|
|
7
|
+
npx pogl deploy # 게임 폴더(index.html이 있는 곳)에서 실행
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- 첫 `deploy`는 AI 검토·개조 판을 만듭니다 (새 게임/기존 게임 연결 선택).
|
|
11
|
+
- 그 뒤의 `deploy`는 그 게임에 바로 재배포됩니다 — 승인 없음.
|
|
12
|
+
- 사이트 주소는 `POGL_SITE` 환경변수로 바꿀 수 있습니다 (기본 https://play.pogl.com).
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pogl-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pogl CLI — deploy your web game like Vercel (npx pogl-cli login / deploy)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pogl": "src/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/poglcom/pogl.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://play.pogl.com",
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pogl CLI — vercel처럼 로그인하고 배포한다 (2026-08-30 유저 방향).
|
|
4
|
+
*
|
|
5
|
+
* npx pogl login 브라우저가 열리고, 승인하면 연동 끝 (vercel login 방식)
|
|
6
|
+
* npx pogl deploy 지금 디렉토리를 배포 — 처음이면 새로/기존 중 고른다
|
|
7
|
+
* npx pogl logout 토큰 삭제
|
|
8
|
+
*
|
|
9
|
+
* login: CLI가 127.0.0.1 콜백 서버를 열고 /cli-login?port=… 을 브라우저로
|
|
10
|
+
* 연다. 웹에서 승인하면 개인 토큰(pogl_…)이 콜백으로 오고 ~/.pogl/token에
|
|
11
|
+
* 저장된다 — 평문 토큰이 서버 DB에는 해시로만 남는다.
|
|
12
|
+
* deploy: 처음이면 [새 게임 / 기존 게임에 연결]을 묻고 ./.pogl.json에
|
|
13
|
+
* 연결을 남긴다. 그 뒤는 그 게임에 바로 재배포된다.
|
|
14
|
+
* 의존성 0 — zip은 시스템 `zip`으로 만든다.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync, execFile } from 'node:child_process'
|
|
17
|
+
import { createServer } from 'node:http'
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { homedir, tmpdir } from 'node:os'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
import { createInterface } from 'node:readline'
|
|
22
|
+
|
|
23
|
+
const SITE = process.env.POGL_SITE ?? 'https://play.pogl.com'
|
|
24
|
+
const TOKEN_FILE = join(homedir(), '.pogl', 'token')
|
|
25
|
+
const LINK_FILE = '.pogl.json'
|
|
26
|
+
|
|
27
|
+
const fail = (msg) => {
|
|
28
|
+
console.error(`pogl: ${msg}`)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const token = () => {
|
|
33
|
+
if (process.env.POGL_TOKEN) return process.env.POGL_TOKEN
|
|
34
|
+
if (existsSync(TOKEN_FILE)) return readFileSync(TOKEN_FILE, 'utf8').trim()
|
|
35
|
+
fail('연동이 안 되어 있습니다 — `npx pogl login` 먼저')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const openBrowser = (url) => {
|
|
39
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
|
|
40
|
+
execFile(cmd, [url], () => {})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ask = (question) =>
|
|
44
|
+
new Promise((resolve) => {
|
|
45
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
46
|
+
rl.question(question, (answer) => {
|
|
47
|
+
rl.close()
|
|
48
|
+
resolve(answer.trim())
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const login = async () => {
|
|
53
|
+
const received = new Promise((resolve) => {
|
|
54
|
+
const server = createServer((req, res) => {
|
|
55
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1')
|
|
56
|
+
if (url.pathname !== '/callback') {
|
|
57
|
+
res.writeHead(404).end()
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' })
|
|
61
|
+
res.end('연동됐습니다 — 터미널로 돌아가세요.')
|
|
62
|
+
resolve({ token: url.searchParams.get('token'), server })
|
|
63
|
+
})
|
|
64
|
+
server.listen(0, '127.0.0.1', () => {
|
|
65
|
+
const { port } = server.address()
|
|
66
|
+
const url = `${SITE}/cli-login?port=${port}`
|
|
67
|
+
console.log(`브라우저에서 승인해 주세요: ${url}`)
|
|
68
|
+
openBrowser(url)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve(null), 180_000))
|
|
72
|
+
const result = await Promise.race([received, timeout])
|
|
73
|
+
if (result === null || !result.token) fail('3분 안에 승인이 오지 않았습니다 — 다시 시도하세요')
|
|
74
|
+
result.server.close()
|
|
75
|
+
mkdirSync(join(homedir(), '.pogl'), { recursive: true })
|
|
76
|
+
writeFileSync(TOKEN_FILE, result.token, 'utf8')
|
|
77
|
+
console.log('연동 완료')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const zipCwd = () => {
|
|
81
|
+
if (!existsSync('index.html')) fail('이 디렉토리에 index.html이 없습니다 — 게임 루트에서 실행하세요')
|
|
82
|
+
const out = join(tmpdir(), `pogl-deploy-${Date.now()}.zip`)
|
|
83
|
+
execFileSync('zip', ['-r', '-q', out, '.', '-x', 'node_modules/*', '.git/*', '.pogl.json'], {
|
|
84
|
+
stdio: 'inherit',
|
|
85
|
+
})
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const post = async (path, body) => {
|
|
90
|
+
const res = await fetch(`${SITE}${path}`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { Authorization: `Bearer ${token()}` },
|
|
93
|
+
body,
|
|
94
|
+
})
|
|
95
|
+
const json = await res.json().catch(() => ({}))
|
|
96
|
+
if (!res.ok) fail(json.error ?? `HTTP ${res.status}`)
|
|
97
|
+
return json
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const deploy = async () => {
|
|
101
|
+
let link = existsSync(LINK_FILE) ? JSON.parse(readFileSync(LINK_FILE, 'utf8')) : null
|
|
102
|
+
if (!link?.gameId) {
|
|
103
|
+
// vercel처럼: 새로 만들지, 기존에 연결할지 묻는다.
|
|
104
|
+
const answer = await ask('기존 게임에 연결하려면 게임 id(또는 스튜디오 URL)를, 새로 만들려면 그냥 Enter: ')
|
|
105
|
+
const idMatch = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.exec(answer)
|
|
106
|
+
if (idMatch !== null) {
|
|
107
|
+
link = { gameId: idMatch[0] }
|
|
108
|
+
writeFileSync(LINK_FILE, JSON.stringify(link, null, 2))
|
|
109
|
+
console.log(`연결됨 → ${link.gameId}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const zip = zipCwd()
|
|
113
|
+
try {
|
|
114
|
+
const form = new FormData()
|
|
115
|
+
form.set('file', new Blob([readFileSync(zip)], { type: 'application/zip' }), 'game.zip')
|
|
116
|
+
if (link?.gameId) {
|
|
117
|
+
console.log(`재배포 → ${link.gameId}`)
|
|
118
|
+
await post(`/api/games/${link.gameId}/deploy`, form)
|
|
119
|
+
console.log(`완료 — ${SITE}/games/${link.gameId}`)
|
|
120
|
+
} else {
|
|
121
|
+
console.log('첫 배포 — AI 검토·개조 판을 만듭니다')
|
|
122
|
+
const { gameId } = await post('/api/games/upload', form)
|
|
123
|
+
writeFileSync(LINK_FILE, JSON.stringify({ gameId }, null, 2))
|
|
124
|
+
console.log(`접수됨 — 검토가 끝나면 승인하세요: ${SITE}/studio?game=${gameId}`)
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
rmSync(zip, { force: true })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const [, , cmd] = process.argv
|
|
132
|
+
if (cmd === 'login') await login()
|
|
133
|
+
else if (cmd === 'logout') {
|
|
134
|
+
rmSync(TOKEN_FILE, { force: true })
|
|
135
|
+
console.log('연동 해제됨')
|
|
136
|
+
} else if (cmd === 'deploy') await deploy()
|
|
137
|
+
else {
|
|
138
|
+
console.log('사용법: pogl login | pogl deploy | pogl logout')
|
|
139
|
+
process.exit(cmd === undefined ? 0 : 1)
|
|
140
|
+
}
|