gamealgo-cli 0.1.7 → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dirichlet AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,75 +1,57 @@
1
- # GameAlgo Client
1
+ # GameAlgo CLI
2
2
 
3
- GameAlgo Client 是公开客户端仓库,包含 iOS SDK、Android SDK、REST helper、TapTap Maker / Lua SDK、CLI 和协议定义。
3
+ `gamealgo-cli` is the independently released command-line client for GameAlgo. It is intended for AI coding agents and game developers to manage integration plans, event dictionaries, reports, scripts, configuration, keys, and experiments.
4
4
 
5
- 产品能力、接入流程、埋点、实验、报表与优化方法由 GameAlgo Server 在线文档统一维护,不在本仓库保留副本。
5
+ Source repository: <https://github.com/sunyuhao-code/GameAlgoCli>
6
6
 
7
- ## AI Agent 从这里开始
8
-
9
- 不要把 README 原样转发给开发者。先安装最新 CLI,再从目标环境读取当前文档并完成接入。
10
-
11
- | 环境 | Admin host | SDK / REST API |
12
- | --- | --- | --- |
13
- | 国内 | `https://game-algo-admin.dictapis.cn` | `https://game-algo-sdk.dictapis.cn` |
14
- | 海外 | `https://dirichlet.ai/algo_admin` | `https://dirichlet.ai/algo_sdk` |
7
+ ## Install
15
8
 
16
9
  ```bash
17
10
  npm install -g gamealgo-cli@latest
18
- gamealgo --version
19
- gamealgo docs agent-onboarding --host <admin-host>
11
+ gamealgo version
20
12
  ```
21
13
 
22
- 开发者通常只需要提供游戏维度的 Game Admin Key,格式为 `ga_admin_*`。在游戏项目根目录登录后,AI Agent 应通过 CLI 创建或读取运行时使用的 `ga_live_*`,完成 SDK 接入、事件验证、Report Pack 和实验配置。
14
+ ## Login
15
+
16
+ Run login from the game repository that should own the credentials:
23
17
 
24
18
  ```bash
25
- gamealgo login --host <admin-host> --admin-key <ga_admin_xxx>
26
- gamealgo integration get-plan --environment <domestic|overseas> --platform <ios|android|maker|rest> --out gamealgo-integration-plan.json
27
- gamealgo key list --json
28
- gamealgo key create --name <runtime-name> --json
29
- gamealgo key reveal --name <runtime-name> --json
19
+ gamealgo login --host <admin-url> --admin-key <ga_admin_key>
20
+ gamealgo whoami
30
21
  ```
31
22
 
32
- AI Agent 必须先执行 `integration get-plan`,并按返回的完整清单完成接入和人工验收。不要依赖只读某一份 README 后自行推断其他平台规则。
23
+ The login is stored in the current project's `.gamealgo/cli.json`, which is automatically ignored by Git. Authenticated commands require this project-scoped login and verify its `gameId` against the Server before continuing, preventing credentials from another game from being reused accidentally.
33
24
 
34
- 凭据保存在当前游戏项目的 `.gamealgo/cli.json`,不同游戏互不覆盖。需要其他主题时使用:
25
+ ## Current help and documentation
35
26
 
36
27
  ```bash
37
- gamealgo docs --host <admin-host>
38
- gamealgo docs integration-guide --host <admin-host>
39
- gamealgo docs ai-tracking-manual --host <admin-host>
40
- gamealgo docs report-packs --host <admin-host>
41
- gamealgo docs experiment-integration-versions --host <admin-host>
42
- gamealgo docs agent-cli --host <admin-host>
28
+ gamealgo help
29
+ gamealgo help report publish
30
+ gamealgo docs
31
+ gamealgo docs sdk-integration
32
+ gamealgo listapi
33
+ gamealgo docs api GameAlgo.Init --platform lua
43
34
  ```
44
35
 
45
- ## SDK 入口
36
+ Help and documentation are fetched from the configured Server on every invocation. The CLI intentionally does not keep a documentation cache.
46
37
 
47
- - [iOS SDK](./ios/README.md)
48
- - [Android SDK](./android/README.md)
49
- - [REST helper](./rest-api/README.md)
50
- - [TapTap Maker / Lua SDK](./lua/README.md)
51
- - [CLI](./cli/README.md)
52
- - [Protocol OpenAPI](./protocol/openapi.yaml)
38
+ Adjust S2S event forwarding can be configured with an explicit event allowlist:
53
39
 
54
- 海外 SDK 地址包含 `/algo_sdk` 路径前缀,必须完整保留。客户端只能配置 `ga_live_*`;`ga_admin_*` 只允许用于开发机器、AI Agent 或 CI。
40
+ ```bash
41
+ gamealgo marketing adjust configure \
42
+ --app-token "$ADJUST_APP_TOKEN" \
43
+ --s2s-token "$ADJUST_S2S_TOKEN" \
44
+ --s2s-events adjust-s2s-events.yaml \
45
+ --json
46
+ ```
55
47
 
56
- ## 仓库结构
48
+ ## Compatibility
57
49
 
58
- ```text
59
- ios/ iOS Swift Package SDK
60
- android/ Android Java SDK core
61
- rest-api/ REST API helper 和示例
62
- lua/ TapTap Maker / Lua SDK
63
- cli/ GameAlgo CLI
64
- protocol/ 客户端协议定义
65
- examples/ 接入示例
66
- ```
50
+ Before login and every authenticated command, the CLI checks `/admin/v1/cli/meta`. An outdated CLI is rejected with the exact npm upgrade command. The CLI and Server are released independently; compatibility is defined by the Server's minimum supported CLI version and protocol version.
67
51
 
68
- ## 本地验证
52
+ ## Development
69
53
 
70
54
  ```bash
71
- npm install
55
+ npm ci
72
56
  npm run check
73
57
  ```
74
-
75
- 平台 SDK README 只描述与当前代码版本绑定的安装方式、公开 API 和运行时约束。业务流程与平台规则以 `gamealgo docs` 返回的在线文档为准。
package/dist/args.js ADDED
@@ -0,0 +1,80 @@
1
+ export function parseGlobalFlags(args) {
2
+ const global = {
3
+ json: false,
4
+ host: process.env.GAMEALGO_ADMIN_HOST || process.env.GAMEALGO_HOST || "",
5
+ adminKey: process.env.GAMEALGO_GAME_ADMIN_KEY || "",
6
+ adminToken: process.env.GAMEALGO_ADMIN_TOKEN || "",
7
+ };
8
+ for (let index = 0; index < args.length;) {
9
+ const arg = args[index];
10
+ if (arg === "--json") {
11
+ global.json = true;
12
+ args.splice(index, 1);
13
+ continue;
14
+ }
15
+ if (arg === "--host") {
16
+ global.host = requireFlagValue(args, index, "--host");
17
+ args.splice(index, 2);
18
+ continue;
19
+ }
20
+ if (arg === "--admin-key") {
21
+ global.adminKey = requireFlagValue(args, index, "--admin-key");
22
+ args.splice(index, 2);
23
+ continue;
24
+ }
25
+ if (arg === "--admin-token") {
26
+ global.adminToken = requireFlagValue(args, index, "--admin-token");
27
+ args.splice(index, 2);
28
+ continue;
29
+ }
30
+ index += 1;
31
+ }
32
+ return global;
33
+ }
34
+ export function parseFlags(args) {
35
+ const flags = {};
36
+ for (let index = 0; index < args.length;) {
37
+ const arg = args[index];
38
+ if (!arg.startsWith("--")) {
39
+ index += 1;
40
+ continue;
41
+ }
42
+ const key = arg.slice(2);
43
+ const next = args[index + 1];
44
+ if (!next || next.startsWith("--")) {
45
+ flags[key] = true;
46
+ args.splice(index, 1);
47
+ }
48
+ else {
49
+ flags[key] = next;
50
+ args.splice(index, 2);
51
+ }
52
+ }
53
+ return flags;
54
+ }
55
+ export function parseSelectorFlags(args) {
56
+ const selectors = {};
57
+ for (let index = 0; index < args.length;) {
58
+ if (args[index] !== "--selector") {
59
+ index += 1;
60
+ continue;
61
+ }
62
+ const value = requireFlagValue(args, index, "--selector");
63
+ const separator = value.indexOf("=");
64
+ if (separator <= 0)
65
+ throw new Error("--selector must use key=value");
66
+ const key = value.slice(0, separator).trim();
67
+ const selectorValue = value.slice(separator + 1).trim();
68
+ if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(key))
69
+ throw new Error(`invalid selector id: ${key}`);
70
+ selectors[key] = selectorValue;
71
+ args.splice(index, 2);
72
+ }
73
+ return selectors;
74
+ }
75
+ export function requireFlagValue(args, index, flag) {
76
+ const value = args[index + 1];
77
+ if (!value || value.startsWith("--"))
78
+ throw new Error(`${flag} requires a value`);
79
+ return value;
80
+ }
package/dist/client.js ADDED
@@ -0,0 +1,233 @@
1
+ import { requestJson } from "./http.js";
2
+ import { normalizeHost } from "./utils.js";
3
+ export class GameAlgoAdminClient {
4
+ host;
5
+ adminKey;
6
+ sessionCache;
7
+ expectedGameId;
8
+ constructor(config, expectedGameId) {
9
+ this.host = normalizeHost(config.host);
10
+ this.adminKey = config.adminKey;
11
+ this.expectedGameId = expectedGameId;
12
+ }
13
+ async session() {
14
+ this.sessionCache ??= await this.get("/admin/v1/session");
15
+ return this.sessionCache;
16
+ }
17
+ async gameId() {
18
+ const session = await this.session();
19
+ const gameId = session.principal.gameId;
20
+ if (!gameId)
21
+ throw new Error("current credential is not scoped to a game");
22
+ if (this.expectedGameId && gameId !== this.expectedGameId) {
23
+ throw new Error(`project is bound to game ${this.expectedGameId}, but the current credential belongs to ${gameId}; run gamealgo login with the correct Game Admin Key`);
24
+ }
25
+ return gameId;
26
+ }
27
+ async assertProjectBinding() {
28
+ await this.gameId();
29
+ }
30
+ async listExperimentV2Strategies(includeArchived = false) {
31
+ const query = includeArchived ? "?includeArchived=1" : "";
32
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2${query}`);
33
+ }
34
+ async listExperimentIntegrationVersions(limit = 100) {
35
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-integration-versions?limit=${encodeURIComponent(String(limit))}`);
36
+ }
37
+ async getLatestExperimentIntegrationVersion() {
38
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-integration-versions/latest`);
39
+ }
40
+ async getExperimentIntegrationVersion(version) {
41
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-integration-versions/${version}`);
42
+ }
43
+ async createExperimentIntegrationVersion(title, message) {
44
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-integration-versions`, { title, message });
45
+ }
46
+ async updateExperimentIntegrationVersion(version, message) {
47
+ return await this.patch(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-integration-versions/${version}`, { message });
48
+ }
49
+ async getExperimentV2Strategy(strategyKey) {
50
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2/${encodeURIComponent(strategyKey)}`);
51
+ }
52
+ async upsertExperimentV2Strategy(body) {
53
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2`, body);
54
+ }
55
+ async updateExperimentV2StrategyDefault(strategyKey, body) {
56
+ return await this.put(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2/${encodeURIComponent(strategyKey)}/default`, body);
57
+ }
58
+ async archiveExperimentV2Strategy(strategyKey, archived) {
59
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2/${encodeURIComponent(strategyKey)}/archive`, { archived });
60
+ }
61
+ async rollbackExperimentV2Strategy(strategyKey, versionId, message) {
62
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2/${encodeURIComponent(strategyKey)}/rollback`, {
63
+ versionId,
64
+ message,
65
+ });
66
+ }
67
+ async createExperimentV2Run(strategyKey, body) {
68
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2/${encodeURIComponent(strategyKey)}/runs`, body);
69
+ }
70
+ async getExperimentV2Run(runId) {
71
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}`);
72
+ }
73
+ async evaluateExperimentV2Run(runId, query) {
74
+ const params = new URLSearchParams();
75
+ if (query.from)
76
+ params.set("from", query.from);
77
+ if (query.to)
78
+ params.set("to", query.to);
79
+ if (query.days)
80
+ params.set("days", query.days);
81
+ if (query.platform)
82
+ params.set("platform", query.platform);
83
+ const suffix = params.toString() ? `?${params.toString()}` : "";
84
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/evaluate${suffix}`, {});
85
+ }
86
+ async promoteExperimentV2Run(runId, variantId, message) {
87
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/promote`, {
88
+ variantId,
89
+ message,
90
+ });
91
+ }
92
+ async cancelExperimentV2Run(runId, message) {
93
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/cancel`, { message });
94
+ }
95
+ async updateExperimentV2RunObjective(runId, objectiveTemplate, message) {
96
+ return await this.patch(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/objective`, {
97
+ objectiveTemplate,
98
+ message,
99
+ });
100
+ }
101
+ async listExperimentV2Overrides(runId) {
102
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/overrides`);
103
+ }
104
+ async upsertExperimentV2Override(runId, userId, variantId) {
105
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/overrides`, { userId, variantId });
106
+ }
107
+ async deleteExperimentV2Override(runId, userId) {
108
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/experiment-runs-v2/${encodeURIComponent(runId)}/overrides/delete`, { userId });
109
+ }
110
+ async listConfigFiles() {
111
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/config-files`);
112
+ }
113
+ async getConfigFile(name) {
114
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/config-files/${encodeURIComponent(name)}`);
115
+ }
116
+ async putConfigFile(name, content, contentType) {
117
+ return await this.put(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/config-files/${encodeURIComponent(name)}`, { content, contentType });
118
+ }
119
+ async listScriptVersions(name, limit = 50) {
120
+ const params = new URLSearchParams();
121
+ if (name)
122
+ params.set("name", name);
123
+ params.set("limit", String(limit));
124
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/script-versions?${params.toString()}`);
125
+ }
126
+ async createScriptVersion(name, content, contentType, message) {
127
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/scripts/${encodeURIComponent(name)}/versions`, {
128
+ content,
129
+ contentType,
130
+ ...(message ? { message } : {}),
131
+ });
132
+ }
133
+ async getScriptVersion(name, versionId) {
134
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/scripts/${encodeURIComponent(name)}/versions/${encodeURIComponent(versionId)}`);
135
+ }
136
+ async getReportPack() {
137
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/report-pack`);
138
+ }
139
+ async previewReportPack(content) {
140
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/report-pack/preview`, { content });
141
+ }
142
+ async putReportPack(content) {
143
+ return await this.put(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/report-pack`, { content });
144
+ }
145
+ async reportManifest() {
146
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/reports/manifest`);
147
+ }
148
+ async queryReportDashboard(body, options = {}) {
149
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/reports/query`, body, options);
150
+ }
151
+ async previewReportDashboard(body, options = {}) {
152
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/reports/preview`, body, options);
153
+ }
154
+ async getGamePreferences() {
155
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/preferences`);
156
+ }
157
+ async countEventPlatforms(startDate, endDate) {
158
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/platform-counts`, { startDate, endDate });
159
+ }
160
+ async countEvents(body, options = {}) {
161
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/count`, body, options);
162
+ }
163
+ async listEventCatalog(days = 30, diagnostics = true) {
164
+ const params = new URLSearchParams({ days: String(days), diagnostics: diagnostics ? "1" : "0" });
165
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/catalog?${params.toString()}`);
166
+ }
167
+ async getEventCatalog(eventType) {
168
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/catalog/${encodeURIComponent(eventType)}`);
169
+ }
170
+ async applyEventCatalog(content) {
171
+ return await this.put(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/catalog`, content);
172
+ }
173
+ async deprecateEventCatalog(eventType) {
174
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/catalog/${encodeURIComponent(eventType)}/deprecate`, {});
175
+ }
176
+ async getIntegrationPlan(environment, platform) {
177
+ const params = new URLSearchParams({ environment, platform });
178
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/integration/plan?${params.toString()}`);
179
+ }
180
+ async getAdjustMarketing(platform) {
181
+ const query = platform ? `?platform=${encodeURIComponent(platform)}` : "";
182
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/marketing/adjust${query}`);
183
+ }
184
+ async putAdjustMarketing(body) {
185
+ return await this.put(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/marketing/adjust`, body);
186
+ }
187
+ async syncAdjustMarketing(body, options = {}) {
188
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/marketing/adjust/sync`, body, options);
189
+ }
190
+ async listGameKeys() {
191
+ return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys`);
192
+ }
193
+ async createGameKey(name) {
194
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys`, { name });
195
+ }
196
+ async revealGameKey(selector) {
197
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys/reveal`, selector);
198
+ }
199
+ async revokeGameKey(id) {
200
+ return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys/${encodeURIComponent(id)}/revoke`, {});
201
+ }
202
+ async get(path) {
203
+ return await requestJson(this.host, path, { headers: this.headers() });
204
+ }
205
+ async post(path, body, options = {}) {
206
+ return await requestJson(this.host, path, {
207
+ method: "POST",
208
+ headers: this.headers({ "Content-Type": "application/json" }),
209
+ body: JSON.stringify(body),
210
+ timeoutMs: options.timeoutMs,
211
+ });
212
+ }
213
+ async put(path, body) {
214
+ return await requestJson(this.host, path, {
215
+ method: "PUT",
216
+ headers: this.headers({ "Content-Type": "application/json" }),
217
+ body: JSON.stringify(body),
218
+ });
219
+ }
220
+ async patch(path, body) {
221
+ return await requestJson(this.host, path, {
222
+ method: "PATCH",
223
+ headers: this.headers({ "Content-Type": "application/json" }),
224
+ body: JSON.stringify(body),
225
+ });
226
+ }
227
+ headers(extra = {}) {
228
+ return {
229
+ "X-GameAlgo-Game-Admin-Key": this.adminKey,
230
+ ...extra,
231
+ };
232
+ }
233
+ }
@@ -0,0 +1,75 @@
1
+ import { parseFlags } from "../args.js";
2
+ import { GameAlgoAdminClient } from "../client.js";
3
+ import { assertServerCompatibility } from "../compatibility.js";
4
+ import { loadProjectConfig, saveConfig } from "../config.js";
5
+ import { requestJson } from "../http.js";
6
+ import { printResult } from "../output.js";
7
+ import { normalizeHost } from "../utils.js";
8
+ export async function login(args, global) {
9
+ const flags = parseFlags(args);
10
+ const host = normalizeHost(String(flags.host || global.host || ""));
11
+ const adminKey = String(flags["admin-key"] || global.adminKey || "");
12
+ if (!host)
13
+ throw new Error("--host is required");
14
+ if (!adminKey)
15
+ throw new Error("--admin-key is required");
16
+ await assertServerCompatibility(host);
17
+ const client = new GameAlgoAdminClient({ host, adminKey });
18
+ const session = await client.session();
19
+ if (session.principal.type !== "game_admin" || !session.principal.gameId) {
20
+ throw new Error("admin key is not scoped to a game");
21
+ }
22
+ const configPath = await saveConfig({ host, adminKey, gameId: session.principal.gameId });
23
+ await printResult({
24
+ ok: true,
25
+ host,
26
+ gameId: session.principal.gameId,
27
+ keyName: session.principal.keyName,
28
+ configPath,
29
+ }, global);
30
+ }
31
+ export async function createClient(global) {
32
+ const saved = await loadProjectConfig();
33
+ if (!saved) {
34
+ throw new Error("this project is not logged in; run `gamealgo login --host <url> --admin-key <key>` in the project directory");
35
+ }
36
+ if (!saved.gameId) {
37
+ throw new Error("this project's CLI login is not bound to a game; re-run `gamealgo login --host <url> --admin-key <key>` in the project directory");
38
+ }
39
+ const host = normalizeHost(global.host || saved?.host || "");
40
+ const adminKey = global.adminKey || saved?.adminKey || "";
41
+ if (!host || !adminKey) {
42
+ throw new Error("run `gamealgo login --host <url> --admin-key <key>` first");
43
+ }
44
+ await assertServerCompatibility(host);
45
+ const client = new GameAlgoAdminClient({ host, adminKey }, saved.gameId);
46
+ await client.assertProjectBinding();
47
+ return client;
48
+ }
49
+ export async function handleAdminKey(args, global) {
50
+ const sub = args.shift();
51
+ if (sub !== "create")
52
+ throw new Error("usage: gamealgo admin-key create --host <url> --admin-token <token> --game <gameId> --name <name>");
53
+ const flags = parseFlags(args);
54
+ const host = normalizeHost(String(flags.host || global.host || ""));
55
+ const adminToken = String(flags["admin-token"] || global.adminToken || "");
56
+ const gameId = String(flags.game || "");
57
+ const name = String(flags.name || "");
58
+ if (!host)
59
+ throw new Error("--host is required");
60
+ if (!adminToken)
61
+ throw new Error("--admin-token is required");
62
+ if (!gameId)
63
+ throw new Error("--game is required");
64
+ if (!name)
65
+ throw new Error("--name is required");
66
+ const response = await requestJson(host, `/admin/v1/games/${encodeURIComponent(gameId)}/admin-keys`, {
67
+ method: "POST",
68
+ headers: {
69
+ "X-GameAlgo-Admin-Token": adminToken,
70
+ "Content-Type": "application/json",
71
+ },
72
+ body: JSON.stringify({ name }),
73
+ });
74
+ await printResult(response, global);
75
+ }
@@ -0,0 +1,79 @@
1
+ import YAML from "yaml";
2
+ import { parseFlags } from "../args.js";
3
+ import { confirm, formatDuration, printResult, reportTimeoutMs, startProgress, withCliMeta } from "../output.js";
4
+ import { readObjectFile, writeTextFile } from "../resources.js";
5
+ import { announceTargetGame } from "../target.js";
6
+ import { optionalString } from "../utils.js";
7
+ export async function handleEvents(client, args, global) {
8
+ const sub = args.shift();
9
+ const flags = parseFlags(args);
10
+ if (sub === "list") {
11
+ if (args.length)
12
+ throw new Error("usage: gamealgo events list [--days 30] [--no-diagnostics]");
13
+ const days = Number(flags.days || 30);
14
+ if (!Number.isInteger(days) || days < 1 || days > 90)
15
+ throw new Error("--days must be an integer between 1 and 90");
16
+ await writeEventCatalogOutput(await client.listEventCatalog(days, flags["no-diagnostics"] !== true), flags, global);
17
+ return;
18
+ }
19
+ if (sub === "get") {
20
+ const eventType = args.shift();
21
+ if (!eventType || args.length)
22
+ throw new Error("usage: gamealgo events get <eventType>");
23
+ await writeEventCatalogOutput(await client.getEventCatalog(eventType), flags, global);
24
+ return;
25
+ }
26
+ if (sub === "apply") {
27
+ const filePath = args.shift();
28
+ if (!filePath || args.length)
29
+ throw new Error("usage: gamealgo events apply <events.yaml>");
30
+ const content = await readObjectFile(filePath, "event catalog");
31
+ await announceTargetGame(client);
32
+ await writeEventCatalogOutput(await client.applyEventCatalog(content), flags, global);
33
+ return;
34
+ }
35
+ if (sub === "deprecate") {
36
+ const eventType = args.shift();
37
+ if (!eventType || args.length)
38
+ throw new Error("usage: gamealgo events deprecate <eventType> [--yes]");
39
+ if (!flags.yes && !(await confirm(`Deprecate event ${eventType}?`)))
40
+ throw new Error("Cancelled");
41
+ await writeEventCatalogOutput(await client.deprecateEventCatalog(eventType), flags, global);
42
+ return;
43
+ }
44
+ if (sub !== "count")
45
+ throw new Error("usage: gamealgo events <list|get|apply|deprecate|count>");
46
+ const timeoutMs = reportTimeoutMs(flags);
47
+ const startedAt = Date.now();
48
+ const stopProgress = startProgress("Querying event counts", timeoutMs);
49
+ let response;
50
+ try {
51
+ response = await client.countEvents({
52
+ startDate: optionalString(flags.from || flags.start || flags.startDate),
53
+ endDate: optionalString(flags.to || flags.end || flags.endDate),
54
+ eventType: optionalString(flags["event-type"] || flags.eventType),
55
+ }, { timeoutMs });
56
+ }
57
+ finally {
58
+ stopProgress();
59
+ }
60
+ const elapsedMs = Date.now() - startedAt;
61
+ const outputValue = withCliMeta(response, { elapsedMs, timeoutMs });
62
+ process.stderr.write(`Event count query finished in ${formatDuration(elapsedMs)}.\n`);
63
+ if (flags.out) {
64
+ await writeTextFile(String(flags.out), JSON.stringify(outputValue, null, 2) + "\n");
65
+ await printResult({ ok: true, out: String(flags.out), elapsedMs }, global);
66
+ return;
67
+ }
68
+ await printResult(outputValue, global);
69
+ }
70
+ export async function writeEventCatalogOutput(value, flags, global) {
71
+ if (flags.out) {
72
+ const filePath = String(flags.out);
73
+ const content = filePath.endsWith(".json") ? JSON.stringify(value, null, 2) + "\n" : YAML.stringify(value);
74
+ await writeTextFile(filePath, content);
75
+ await printResult({ ok: true, out: filePath }, global);
76
+ return;
77
+ }
78
+ await printResult(value, global);
79
+ }