gamealgo-cli 0.1.6 → 0.2.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/LICENSE +21 -0
- package/README.md +32 -47
- package/dist/args.js +80 -0
- package/dist/client.js +232 -0
- package/dist/commands/auth.js +75 -0
- package/dist/commands/events.js +79 -0
- package/dist/commands/experiment.js +373 -0
- package/dist/commands/integration.js +30 -0
- package/dist/commands/keys.js +113 -0
- package/dist/commands/marketing.js +79 -0
- package/dist/commands/report.js +175 -0
- package/dist/commands/resources.js +95 -0
- package/dist/compatibility.js +62 -0
- package/dist/config.js +80 -0
- package/dist/docs.js +606 -0
- package/dist/http.js +54 -0
- package/dist/main.js +106 -0
- package/dist/output.js +82 -0
- package/dist/resources.js +93 -0
- package/dist/target.js +3 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +23 -0
- package/package.json +16 -7
- package/cli/README.md +0 -433
- package/dist/gamealgo.js +0 -1779
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,72 +1,57 @@
|
|
|
1
|
-
# GameAlgo
|
|
1
|
+
# GameAlgo CLI
|
|
2
2
|
|
|
3
|
-
GameAlgo
|
|
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
|
-
|
|
5
|
+
Source repository: <https://github.com/sunyuhao-code/GameAlgoCli>
|
|
6
6
|
|
|
7
|
-
##
|
|
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
|
|
19
|
-
gamealgo docs agent-onboarding --host <admin-host>
|
|
11
|
+
gamealgo version
|
|
20
12
|
```
|
|
21
13
|
|
|
22
|
-
|
|
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-
|
|
26
|
-
gamealgo
|
|
27
|
-
gamealgo key create --name <runtime-name> --json
|
|
28
|
-
gamealgo key reveal --name <runtime-name> --json
|
|
19
|
+
gamealgo login --host <admin-url> --admin-key <ga_admin_key>
|
|
20
|
+
gamealgo whoami
|
|
29
21
|
```
|
|
30
22
|
|
|
31
|
-
|
|
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.
|
|
24
|
+
|
|
25
|
+
## Current help and documentation
|
|
32
26
|
|
|
33
27
|
```bash
|
|
34
|
-
gamealgo
|
|
35
|
-
gamealgo
|
|
36
|
-
gamealgo docs
|
|
37
|
-
gamealgo docs
|
|
38
|
-
gamealgo
|
|
39
|
-
gamealgo docs
|
|
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
|
|
40
34
|
```
|
|
41
35
|
|
|
42
|
-
|
|
36
|
+
Help and documentation are fetched from the configured Server on every invocation. The CLI intentionally does not keep a documentation cache.
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
- [Android SDK](./android/README.md)
|
|
46
|
-
- [REST helper](./rest-api/README.md)
|
|
47
|
-
- [TapTap Maker / Lua SDK](./lua/README.md)
|
|
48
|
-
- [CLI](./cli/README.md)
|
|
49
|
-
- [Protocol OpenAPI](./protocol/openapi.yaml)
|
|
38
|
+
Adjust S2S event forwarding can be configured with an explicit event allowlist:
|
|
50
39
|
|
|
51
|
-
|
|
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
|
+
```
|
|
52
47
|
|
|
53
|
-
##
|
|
48
|
+
## Compatibility
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
ios/ iOS Swift Package SDK
|
|
57
|
-
android/ Android Java SDK core
|
|
58
|
-
rest-api/ REST API helper 和示例
|
|
59
|
-
lua/ TapTap Maker / Lua SDK
|
|
60
|
-
cli/ GameAlgo CLI
|
|
61
|
-
protocol/ 客户端协议定义
|
|
62
|
-
examples/ 接入示例
|
|
63
|
-
```
|
|
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.
|
|
64
51
|
|
|
65
|
-
##
|
|
52
|
+
## Development
|
|
66
53
|
|
|
67
54
|
```bash
|
|
68
|
-
npm
|
|
55
|
+
npm ci
|
|
69
56
|
npm run check
|
|
70
57
|
```
|
|
71
|
-
|
|
72
|
-
平台 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,232 @@
|
|
|
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() {
|
|
181
|
+
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/marketing/adjust`);
|
|
182
|
+
}
|
|
183
|
+
async putAdjustMarketing(body) {
|
|
184
|
+
return await this.put(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/marketing/adjust`, body);
|
|
185
|
+
}
|
|
186
|
+
async syncAdjustMarketing(body, options = {}) {
|
|
187
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/marketing/adjust/sync`, body, options);
|
|
188
|
+
}
|
|
189
|
+
async listGameKeys() {
|
|
190
|
+
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys`);
|
|
191
|
+
}
|
|
192
|
+
async createGameKey(name) {
|
|
193
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys`, { name });
|
|
194
|
+
}
|
|
195
|
+
async revealGameKey(selector) {
|
|
196
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys/reveal`, selector);
|
|
197
|
+
}
|
|
198
|
+
async revokeGameKey(id) {
|
|
199
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/keys/${encodeURIComponent(id)}/revoke`, {});
|
|
200
|
+
}
|
|
201
|
+
async get(path) {
|
|
202
|
+
return await requestJson(this.host, path, { headers: this.headers() });
|
|
203
|
+
}
|
|
204
|
+
async post(path, body, options = {}) {
|
|
205
|
+
return await requestJson(this.host, path, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: this.headers({ "Content-Type": "application/json" }),
|
|
208
|
+
body: JSON.stringify(body),
|
|
209
|
+
timeoutMs: options.timeoutMs,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async put(path, body) {
|
|
213
|
+
return await requestJson(this.host, path, {
|
|
214
|
+
method: "PUT",
|
|
215
|
+
headers: this.headers({ "Content-Type": "application/json" }),
|
|
216
|
+
body: JSON.stringify(body),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
async patch(path, body) {
|
|
220
|
+
return await requestJson(this.host, path, {
|
|
221
|
+
method: "PATCH",
|
|
222
|
+
headers: this.headers({ "Content-Type": "application/json" }),
|
|
223
|
+
body: JSON.stringify(body),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
headers(extra = {}) {
|
|
227
|
+
return {
|
|
228
|
+
"X-GameAlgo-Game-Admin-Key": this.adminKey,
|
|
229
|
+
...extra,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -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
|
+
}
|