gamealgo-cli 0.1.7 → 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.
@@ -0,0 +1,373 @@
1
+ import YAML from "yaml";
2
+ import { parseFlags } from "../args.js";
3
+ import { confirm, isInteractive, optionalNonNegativeInteger, positiveInteger, printResult } from "../output.js";
4
+ import { objectValue, printExperimentRunVariantScripts, readObjectFile, validateExperimentRunVariantScripts, writeTextFile } from "../resources.js";
5
+ import { optionalString } from "../utils.js";
6
+ export async function handleExperiment(client, args, global) {
7
+ const sub = args.shift();
8
+ if (sub === "strategies" || sub === "list") {
9
+ const flags = parseFlags(args);
10
+ await printResult(await client.listExperimentV2Strategies(Boolean(flags["include-archived"] || flags.archived)), global);
11
+ return;
12
+ }
13
+ if (sub === "strategy") {
14
+ await handleExperimentStrategy(client, args, global);
15
+ return;
16
+ }
17
+ if (sub === "run") {
18
+ await handleExperimentRun(client, args, global);
19
+ return;
20
+ }
21
+ if (sub === "override") {
22
+ await handleExperimentOverride(client, args, global);
23
+ return;
24
+ }
25
+ if (sub === "integration-version") {
26
+ await handleExperimentIntegrationVersion(client, args, global);
27
+ return;
28
+ }
29
+ throw new Error("usage: gamealgo experiment <strategies|strategy|run|override|integration-version>");
30
+ }
31
+ export async function handleExperimentIntegrationVersion(client, args, global) {
32
+ const sub = args.shift();
33
+ const flags = parseFlags(args);
34
+ if (sub === "latest") {
35
+ await printResult(await client.getLatestExperimentIntegrationVersion(), global);
36
+ return;
37
+ }
38
+ if (sub === "list") {
39
+ const limit = optionalNonNegativeInteger(flags.limit, "--limit") ?? 100;
40
+ await printResult(await client.listExperimentIntegrationVersions(limit), global);
41
+ return;
42
+ }
43
+ if (sub === "get" || sub === "show") {
44
+ const version = positiveInteger(flags.version || args.shift(), "version");
45
+ await printResult(await client.getExperimentIntegrationVersion(version), global);
46
+ return;
47
+ }
48
+ if (sub === "create") {
49
+ const title = optionalString(flags.title);
50
+ const message = optionalString(flags.message);
51
+ if (!title || !message) {
52
+ throw new Error("usage: gamealgo experiment integration-version create --title <title> --message <message>");
53
+ }
54
+ await printResult(await client.createExperimentIntegrationVersion(title, message), global);
55
+ return;
56
+ }
57
+ if (sub === "update") {
58
+ const version = positiveInteger(flags.version || args.shift(), "version");
59
+ const message = optionalString(flags.message);
60
+ if (!message) {
61
+ throw new Error("usage: gamealgo experiment integration-version update --version <version> --message <message>");
62
+ }
63
+ await printResult(await client.updateExperimentIntegrationVersion(version, message), global);
64
+ return;
65
+ }
66
+ throw new Error("usage: gamealgo experiment integration-version <latest|list|get|create|update>");
67
+ }
68
+ export async function handleExperimentStrategy(client, args, global) {
69
+ const sub = args.shift();
70
+ if (sub === "show" || sub === "get") {
71
+ const flags = parseFlags(args);
72
+ const strategyKey = optionalString(flags.strategy || flags.key) || optionalString(args.shift());
73
+ if (!strategyKey)
74
+ throw new Error("usage: gamealgo experiment strategy show <strategyKey>");
75
+ const response = await client.getExperimentV2Strategy(strategyKey);
76
+ if (flags.out) {
77
+ await writeTextFile(String(flags.out), YAML.stringify(response));
78
+ await printResult({ ok: true, out: String(flags.out), strategyKey }, global);
79
+ return;
80
+ }
81
+ await printResult(response, global);
82
+ return;
83
+ }
84
+ if (sub === "publish" || sub === "upsert") {
85
+ const flags = parseFlags(args);
86
+ const filePath = optionalString(flags.file || flags.input) || optionalString(args.shift());
87
+ if (!filePath)
88
+ throw new Error("usage: gamealgo experiment strategy publish <strategy.yaml> --yes");
89
+ await requireYes(flags, global, "experiment strategy publish");
90
+ const body = await readObjectFile(filePath, "experiment strategy");
91
+ if (!optionalString(body.displayName)) {
92
+ throw new Error("experiment strategy requires displayName");
93
+ }
94
+ if (!optionalString(body.strategyKey || body.key)) {
95
+ throw new Error("experiment strategy requires strategyKey");
96
+ }
97
+ const requiredIntegrationVersion = optionalNonNegativeInteger(body.requiredIntegrationVersion, "requiredIntegrationVersion");
98
+ if (requiredIntegrationVersion === undefined) {
99
+ throw new Error("experiment strategy requires explicit requiredIntegrationVersion; use 0 when no client integration version is required");
100
+ }
101
+ const response = await client.upsertExperimentV2Strategy(body);
102
+ await printResult(response, global);
103
+ return;
104
+ }
105
+ if (sub === "default") {
106
+ const flags = parseFlags(args);
107
+ const strategyKey = optionalString(flags.strategy || flags.key) || optionalString(args.shift());
108
+ const filePath = optionalString(flags.file || flags.input) || optionalString(args.shift());
109
+ if (!strategyKey || !filePath)
110
+ throw new Error("usage: gamealgo experiment strategy default <strategyKey> <strategy.yaml> --yes");
111
+ await requireYes(flags, global, "experiment strategy default");
112
+ const response = await client.updateExperimentV2StrategyDefault(strategyKey, await readObjectFile(filePath, "experiment strategy default"));
113
+ await printResult(response, global);
114
+ return;
115
+ }
116
+ if (sub === "archive" || sub === "restore") {
117
+ const flags = parseFlags(args);
118
+ const strategyKey = optionalString(flags.strategy || flags.key) || optionalString(args.shift());
119
+ if (!strategyKey)
120
+ throw new Error(`usage: gamealgo experiment strategy ${sub} <strategyKey> --yes`);
121
+ await requireYes(flags, global, `experiment strategy ${sub}`);
122
+ await printResult(await client.archiveExperimentV2Strategy(strategyKey, sub === "archive"), global);
123
+ return;
124
+ }
125
+ if (sub === "rollback") {
126
+ const flags = parseFlags(args);
127
+ const strategyKey = optionalString(flags.strategy || flags.key) || optionalString(args.shift());
128
+ const versionId = optionalString(flags.version || flags["version-id"]);
129
+ const message = requiredMessage(flags, "experiment strategy rollback");
130
+ if (!strategyKey || !versionId) {
131
+ throw new Error("usage: gamealgo experiment strategy rollback <strategyKey> --version-id <versionId> --message <reason> --yes");
132
+ }
133
+ await requireYes(flags, global, "experiment strategy rollback");
134
+ await printResult(await client.rollbackExperimentV2Strategy(strategyKey, versionId, message), global);
135
+ return;
136
+ }
137
+ throw new Error("usage: gamealgo experiment strategy <show|publish|default|archive|restore|rollback>");
138
+ }
139
+ export async function handleExperimentRun(client, args, global) {
140
+ const sub = args.shift();
141
+ if (sub === "create") {
142
+ const flags = parseFlags(args);
143
+ const strategyKey = optionalString(flags.strategy || flags.key) || optionalString(args.shift());
144
+ const filePath = optionalString(flags.file || flags.input) || optionalString(args.shift());
145
+ if (!strategyKey || !filePath)
146
+ throw new Error("usage: gamealgo experiment run create <strategyKey> <run.yaml> [--manual] --yes");
147
+ await requireYes(flags, global, "experiment run create");
148
+ const body = await readObjectFile(filePath, "experiment run");
149
+ if (!optionalString(body.displayName))
150
+ throw new Error("experiment run requires displayName");
151
+ const objectiveTemplate = optionalString(body.objectiveTemplate);
152
+ if (!objectiveTemplate || !["ltv_proxy@1", "active_days@1"].includes(objectiveTemplate)) {
153
+ throw new Error("experiment run requires objectiveTemplate: ltv_proxy@1 or active_days@1");
154
+ }
155
+ if (!optionalString(body.baselineVariantId)) {
156
+ throw new Error("experiment run requires explicit baselineVariantId");
157
+ }
158
+ validateExperimentRunVariantScripts(body.variants);
159
+ const fileRunType = optionalString(body.type);
160
+ const flagRunType = optionalString(flags.type);
161
+ const manualFlag = flags.manual === true;
162
+ if (fileRunType && fileRunType !== "managed" && fileRunType !== "manual") {
163
+ throw new Error("experiment run type must be managed or manual");
164
+ }
165
+ if (flagRunType && flagRunType !== "managed" && flagRunType !== "manual") {
166
+ throw new Error("--type must be managed or manual");
167
+ }
168
+ if (manualFlag && flagRunType && flagRunType !== "manual") {
169
+ throw new Error("--manual conflicts with --type managed");
170
+ }
171
+ const requestedRunType = manualFlag ? "manual" : flagRunType;
172
+ if (requestedRunType && fileRunType && requestedRunType !== fileRunType) {
173
+ throw new Error(`experiment run type conflict: file uses ${fileRunType}, CLI requests ${requestedRunType}`);
174
+ }
175
+ const runType = requestedRunType || fileRunType || "managed";
176
+ body.type = runType;
177
+ if (runType !== "managed" && body.startCondition !== undefined) {
178
+ throw new Error("experiment run startCondition is supported only when type is managed");
179
+ }
180
+ if (runType === "managed") {
181
+ const minWindowDays = Number(body.minWindowDays);
182
+ const maxWindowDays = Number(body.maxWindowDays);
183
+ if (!Number.isInteger(minWindowDays) || minWindowDays < 1 || minWindowDays > 30) {
184
+ throw new Error("managed experiment run requires minWindowDays between 1 and 30");
185
+ }
186
+ if (!Number.isInteger(maxWindowDays) || maxWindowDays < minWindowDays || maxWindowDays > 60) {
187
+ throw new Error("managed experiment run requires maxWindowDays between minWindowDays and 60");
188
+ }
189
+ }
190
+ else {
191
+ process.stderr.write("Warning: creating a manual experiment. The platform will not automatically extend, conclude, or promote it; use manual mode only when the developer will manage the experiment decision.\n");
192
+ }
193
+ const result = await client.createExperimentV2Run(strategyKey, body);
194
+ await printResult(result, global);
195
+ if (!global.json)
196
+ printExperimentRunVariantScripts(result);
197
+ return;
198
+ }
199
+ if (sub === "show" || sub === "get") {
200
+ const flags = parseFlags(args);
201
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
202
+ if (!runId)
203
+ throw new Error("usage: gamealgo experiment run show <runId>");
204
+ const response = await client.getExperimentV2Run(runId);
205
+ if (flags.out) {
206
+ await writeTextFile(String(flags.out), YAML.stringify(response));
207
+ await printResult({ ok: true, out: String(flags.out), runId }, global);
208
+ return;
209
+ }
210
+ await printResult(response, global);
211
+ return;
212
+ }
213
+ if (sub === "status") {
214
+ const flags = parseFlags(args);
215
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
216
+ if (!runId)
217
+ throw new Error("usage: gamealgo experiment run status <runId>");
218
+ const response = await client.getExperimentV2Run(runId);
219
+ const run = response.run;
220
+ if (!run)
221
+ throw new Error(`experiment run not found: ${runId}`);
222
+ await printResult({
223
+ run: {
224
+ id: run.id,
225
+ strategyKey: run.strategyKey,
226
+ displayName: run.displayName,
227
+ type: run.type,
228
+ status: run.status,
229
+ objective: run.objective,
230
+ baselineVariantId: run.baselineVariantId,
231
+ requiredIntegrationVersion: run.requiredIntegrationVersion,
232
+ startedAt: run.startedAt,
233
+ endedAt: run.endedAt,
234
+ promotedVariantId: run.promotedVariantId,
235
+ currentAssessment: run.currentAssessment ?? null,
236
+ },
237
+ }, global);
238
+ return;
239
+ }
240
+ if (sub === "evaluate") {
241
+ const flags = parseFlags(args);
242
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
243
+ if (!runId)
244
+ throw new Error("usage: gamealgo experiment run evaluate <runId> --from YYYY-MM-DD --to YYYY-MM-DD --yes");
245
+ await requireYes(flags, global, "experiment run evaluate");
246
+ await printResult(await client.evaluateExperimentV2Run(runId, {
247
+ from: optionalString(flags.from || flags.start || flags.startDate),
248
+ to: optionalString(flags.to || flags.end || flags.endDate),
249
+ days: optionalString(flags.days),
250
+ }), global);
251
+ return;
252
+ }
253
+ if (sub === "report") {
254
+ const flags = parseFlags(args);
255
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
256
+ if (!runId)
257
+ throw new Error("usage: gamealgo experiment run report <runId> [--round <number> | --report-id <id>]");
258
+ const response = await client.getExperimentV2Run(runId);
259
+ const reports = Array.isArray(response.run?.reports) ? response.run.reports : [];
260
+ const reportId = optionalString(flags.report || flags["report-id"] || flags.id);
261
+ const round = flags.round === undefined ? undefined : positiveInteger(flags.round, "round");
262
+ const latestReport = objectValue(reports[reports.length - 1]);
263
+ const report = reports
264
+ .map((item) => objectValue(item))
265
+ .find((item) => {
266
+ if (!item)
267
+ return false;
268
+ if (reportId)
269
+ return item.id === reportId || item.evaluationId === reportId;
270
+ if (round !== undefined) {
271
+ const managed = objectValue(objectValue(item.report)?.managed);
272
+ return Number(managed?.roundNumber ?? Number(managed?.roundIndex) + 1) === round;
273
+ }
274
+ return item === latestReport;
275
+ });
276
+ if (!report) {
277
+ const selector = reportId ? `: ${reportId}` : round !== undefined ? ` for round ${round}` : "";
278
+ throw new Error(`experiment run report not found${selector}`);
279
+ }
280
+ await printResult({ runId, report }, global);
281
+ return;
282
+ }
283
+ if (sub === "objective") {
284
+ const flags = parseFlags(args);
285
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
286
+ const objectiveTemplate = optionalString(flags.objective || flags["objective-template"]);
287
+ if (!runId || !objectiveTemplate) {
288
+ throw new Error("usage: gamealgo experiment run objective <runId> --objective <ltv_proxy@1|active_days@1> --message <reason> --yes");
289
+ }
290
+ if (!["ltv_proxy@1", "active_days@1"].includes(objectiveTemplate)) {
291
+ throw new Error("experiment objective must be ltv_proxy@1 or active_days@1");
292
+ }
293
+ const message = requiredMessage(flags, "experiment run objective");
294
+ await requireYes(flags, global, "experiment run objective");
295
+ await printResult(await client.updateExperimentV2RunObjective(runId, objectiveTemplate, message), global);
296
+ return;
297
+ }
298
+ if (sub === "promote") {
299
+ const flags = parseFlags(args);
300
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
301
+ const variantId = optionalString(flags.variant || flags["variant-id"]) || optionalString(args.shift());
302
+ if (!runId || !variantId)
303
+ throw new Error("usage: gamealgo experiment run promote <runId> --variant <variantId> --message <reason> --yes");
304
+ const message = requiredMessage(flags, "experiment run promote");
305
+ await requireYes(flags, global, "experiment run promote");
306
+ await printResult(await client.promoteExperimentV2Run(runId, variantId, message), global);
307
+ return;
308
+ }
309
+ if (sub === "cancel") {
310
+ const flags = parseFlags(args);
311
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
312
+ if (!runId)
313
+ throw new Error("usage: gamealgo experiment run cancel <runId> --message <reason> --yes");
314
+ const message = requiredMessage(flags, "experiment run cancel");
315
+ await requireYes(flags, global, "experiment run cancel");
316
+ await printResult(await client.cancelExperimentV2Run(runId, message), global);
317
+ return;
318
+ }
319
+ throw new Error("usage: gamealgo experiment run <create|show|status|evaluate|report|objective|promote|cancel>");
320
+ }
321
+ export function requiredMessage(flags, action) {
322
+ const message = optionalString(flags.message);
323
+ if (!message)
324
+ throw new Error(`${action} requires --message <reason>`);
325
+ return message;
326
+ }
327
+ export async function handleExperimentOverride(client, args, global) {
328
+ const sub = args.shift();
329
+ if (sub === "list") {
330
+ const flags = parseFlags(args);
331
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
332
+ if (!runId)
333
+ throw new Error("usage: gamealgo experiment override list <runId>");
334
+ await printResult(await client.listExperimentV2Overrides(runId), global);
335
+ return;
336
+ }
337
+ if (sub === "set") {
338
+ const flags = parseFlags(args);
339
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
340
+ const userId = optionalString(flags.user || flags["user-id"] || flags.device || flags["device-id"]);
341
+ const variantId = optionalString(flags.variant || flags["variant-id"]);
342
+ if (!runId || !userId || !variantId)
343
+ throw new Error("usage: gamealgo experiment override set <runId> --user <userId> --variant <variantId> --yes");
344
+ await requireYes(flags, global, "experiment override set");
345
+ await printResult(await client.upsertExperimentV2Override(runId, userId, variantId), global);
346
+ return;
347
+ }
348
+ if (sub === "delete" || sub === "remove") {
349
+ const flags = parseFlags(args);
350
+ const runId = optionalString(flags.run || flags["run-id"] || flags.id) || optionalString(args.shift());
351
+ const userId = optionalString(flags.user || flags["user-id"] || flags.device || flags["device-id"]);
352
+ if (!runId || !userId)
353
+ throw new Error(`usage: gamealgo experiment override ${sub} <runId> --user <userId> --yes`);
354
+ await requireYes(flags, global, `experiment override ${sub}`);
355
+ await printResult(await client.deleteExperimentV2Override(runId, userId), global);
356
+ return;
357
+ }
358
+ throw new Error("usage: gamealgo experiment override <list|set|delete>");
359
+ }
360
+ export async function requireYes(flags, global, action) {
361
+ if (flags.yes)
362
+ return;
363
+ if (global.json) {
364
+ throw new Error(`${action} requires --yes in --json mode`);
365
+ }
366
+ if (!isInteractive()) {
367
+ throw new Error(`${action} requires --yes in non-interactive mode`);
368
+ }
369
+ const ok = await confirm(`Run ${action}?`);
370
+ if (!ok) {
371
+ throw new Error(`${action} cancelled`);
372
+ }
373
+ }
@@ -0,0 +1,30 @@
1
+ import { parseFlags } from "../args.js";
2
+ import { printResult } from "../output.js";
3
+ import { writeTextFile } from "../resources.js";
4
+ import { announceTargetGame } from "../target.js";
5
+ import { integrationEnvironment, integrationPlatform } from "../utils.js";
6
+ export async function handleIntegration(client, args, global) {
7
+ const sub = args.shift();
8
+ const flags = parseFlags(args);
9
+ if (args.length)
10
+ throw new Error("unexpected positional arguments for integration command");
11
+ const environment = integrationEnvironment(flags.environment);
12
+ const platform = integrationPlatform(flags.platform);
13
+ if (sub === "get-plan" || sub === "plan") {
14
+ await announceTargetGame(client);
15
+ const response = await client.getIntegrationPlan(environment, platform);
16
+ await writeIntegrationOutput(response, flags, global);
17
+ return;
18
+ }
19
+ throw new Error("usage: gamealgo integration get-plan --environment <domestic|overseas> --platform <ios|android|maker|rest>");
20
+ }
21
+ export async function writeIntegrationOutput(value, flags, global) {
22
+ if (flags.out) {
23
+ if (typeof flags.out !== "string")
24
+ throw new Error("--out requires a file path");
25
+ await writeTextFile(flags.out, JSON.stringify(value, null, 2) + "\n");
26
+ await printResult({ ok: true, out: flags.out }, global);
27
+ return;
28
+ }
29
+ await printResult(value, global);
30
+ }
@@ -0,0 +1,113 @@
1
+ import { parseFlags } from "../args.js";
2
+ import { confirm, isInteractive, printResult } from "../output.js";
3
+ import { announceTargetGame } from "../target.js";
4
+ import { optionalString } from "../utils.js";
5
+ export async function handleGameKey(client, args, global) {
6
+ const sub = args.shift();
7
+ if (sub === "list") {
8
+ const keys = (await client.listGameKeys()).keys
9
+ .filter((key) => key.status === "active")
10
+ .map(publicGameKeyForCli);
11
+ await printResult({ keys }, global);
12
+ return;
13
+ }
14
+ if (sub === "create") {
15
+ const flags = parseFlags(args);
16
+ const name = optionalString(flags.name) || optionalString(args.shift());
17
+ if (!name)
18
+ throw new Error("usage: gamealgo key create --name <key-name>");
19
+ const response = await client.createGameKey(name);
20
+ await printResult({
21
+ ok: true,
22
+ created: response.created !== false,
23
+ name: response.key.name,
24
+ key: response.rawKey,
25
+ prefix: response.key.keyPrefix,
26
+ keyPrefix: response.key.keyPrefix,
27
+ status: response.key.status,
28
+ createdAt: response.key.createdAt,
29
+ }, global);
30
+ return;
31
+ }
32
+ if (sub === "reveal") {
33
+ await announceTargetGame(client);
34
+ const selector = keySelectorFromArgs(args, "gamealgo key reveal --name <key-name>");
35
+ const response = await client.revealGameKey(selector);
36
+ await printResult({
37
+ ok: true,
38
+ name: response.key.name,
39
+ key: response.rawKey,
40
+ prefix: response.key.keyPrefix,
41
+ keyPrefix: response.key.keyPrefix,
42
+ status: response.key.status,
43
+ createdAt: response.key.createdAt,
44
+ }, global);
45
+ return;
46
+ }
47
+ if (sub === "revoke") {
48
+ const flags = parseFlags(args);
49
+ if (!flags.yes && global.json) {
50
+ throw new Error("key revoke requires --yes in --json mode");
51
+ }
52
+ if (!flags.yes && !isInteractive()) {
53
+ throw new Error("key revoke requires --yes in non-interactive mode");
54
+ }
55
+ const selector = keySelectorFromFlags(flags, args, "gamealgo key revoke --name <key-name> --yes");
56
+ const key = await resolveGameKey(client, selector);
57
+ if (!flags.yes) {
58
+ const ok = await confirm(`Revoke client game key ${key.name} (${key.keyPrefix})?`);
59
+ if (!ok)
60
+ throw new Error("revoke cancelled");
61
+ }
62
+ const response = await client.revokeGameKey(key.id);
63
+ await printResult({ ok: true, key: publicGameKeyForCli(response.key) }, global);
64
+ return;
65
+ }
66
+ throw new Error("usage: gamealgo key <list|create|reveal|revoke>");
67
+ }
68
+ export function publicGameKeyForCli(key) {
69
+ return {
70
+ id: key.id,
71
+ name: key.name,
72
+ prefix: key.keyPrefix,
73
+ keyPrefix: key.keyPrefix,
74
+ status: key.status,
75
+ createdAt: key.createdAt,
76
+ };
77
+ }
78
+ export function keySelectorFromArgs(args, usage) {
79
+ const flags = parseFlags(args);
80
+ return keySelectorFromFlags(flags, args, usage);
81
+ }
82
+ export function keySelectorFromFlags(flags, args, usage) {
83
+ const id = optionalString(flags.id);
84
+ const name = optionalString(flags.name);
85
+ const prefix = optionalString(flags.prefix) || optionalString(flags["key-prefix"]);
86
+ const positionalName = !id && !name && !prefix ? optionalString(args.find((arg) => !arg.startsWith("--"))) : undefined;
87
+ const selector = {
88
+ id,
89
+ name: name || positionalName,
90
+ prefix,
91
+ };
92
+ if (!selector.id && !selector.name && !selector.prefix)
93
+ throw new Error(`usage: ${usage}`);
94
+ return selector;
95
+ }
96
+ export async function resolveGameKey(client, selector) {
97
+ if (selector.id) {
98
+ const key = (await client.listGameKeys()).keys.find((item) => item.status === "active" && item.id === selector.id);
99
+ if (!key)
100
+ throw new Error(`client game key not found: ${selector.id}`);
101
+ return key;
102
+ }
103
+ if (selector.name) {
104
+ const key = (await client.listGameKeys()).keys.find((item) => item.status === "active" && item.name === selector.name);
105
+ if (!key)
106
+ throw new Error(`client game key not found: ${selector.name}`);
107
+ return key;
108
+ }
109
+ const key = (await client.listGameKeys()).keys.find((item) => item.status === "active" && item.keyPrefix === selector.prefix);
110
+ if (!key)
111
+ throw new Error(`client game key not found: ${selector.prefix}`);
112
+ return key;
113
+ }
@@ -0,0 +1,79 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import YAML from "yaml";
3
+ import { parseFlags } from "../args.js";
4
+ import { formatDuration, printResult, reportTimeoutMs, startProgress, withCliMeta } from "../output.js";
5
+ import { objectValue } from "../resources.js";
6
+ import { optionalString } from "../utils.js";
7
+ export async function handleMarketing(client, args, global) {
8
+ const provider = args.shift();
9
+ if (provider !== "adjust")
10
+ throw new Error("usage: gamealgo marketing adjust <get|configure|sync>");
11
+ const sub = args.shift();
12
+ if (sub === "get" || sub === "status") {
13
+ await printResult(await client.getAdjustMarketing(), global);
14
+ return;
15
+ }
16
+ if (sub === "configure" || sub === "set") {
17
+ const flags = parseFlags(args);
18
+ const current = await client.getAdjustMarketing();
19
+ const existing = current.integration || {};
20
+ const appToken = optionalString(flags["app-token"] || flags.appToken || existing.appToken);
21
+ const platform = optionalString(flags.platform || existing.platform) || "ios";
22
+ if (!appToken)
23
+ throw new Error("--app-token is required");
24
+ if (platform !== "ios" && platform !== "android")
25
+ throw new Error("--platform must be ios or android");
26
+ const body = {
27
+ appToken,
28
+ appName: optionalString(flags["app-name"] || flags.appName || existing.appName) || "",
29
+ platform,
30
+ currency: optionalString(flags.currency || existing.currency) || "USD",
31
+ status: optionalString(flags.status || existing.status) || "active",
32
+ };
33
+ const apiToken = optionalString(flags["api-token"] || flags.apiToken);
34
+ if (apiToken)
35
+ body.apiToken = apiToken;
36
+ const s2sToken = optionalString(flags["s2s-token"] || flags.s2sToken);
37
+ if (s2sToken)
38
+ body.s2sToken = s2sToken;
39
+ const s2sEventsPath = optionalString(flags["s2s-events"] || flags.s2sEvents);
40
+ if (s2sEventsPath)
41
+ body.s2sEventMappings = await readAdjustS2sEventMappings(s2sEventsPath);
42
+ const result = objectValue(await client.putAdjustMarketing(body));
43
+ await printResult({ ok: true, ...(result || {}) }, global);
44
+ return;
45
+ }
46
+ if (sub === "sync") {
47
+ const flags = parseFlags(args);
48
+ const timeoutMs = reportTimeoutMs(flags);
49
+ const startedAt = Date.now();
50
+ const stopProgress = startProgress("Syncing Adjust marketing spend", timeoutMs);
51
+ let response;
52
+ try {
53
+ response = await client.syncAdjustMarketing({
54
+ startDate: optionalString(flags.from || flags.start || flags.startDate),
55
+ endDate: optionalString(flags.to || flags.end || flags.endDate),
56
+ }, { timeoutMs });
57
+ }
58
+ finally {
59
+ stopProgress();
60
+ }
61
+ const elapsedMs = Date.now() - startedAt;
62
+ process.stderr.write(`Adjust sync finished in ${formatDuration(elapsedMs)}.\n`);
63
+ await printResult(withCliMeta(response, { elapsedMs, timeoutMs }), global);
64
+ return;
65
+ }
66
+ throw new Error("usage: gamealgo marketing adjust <get|configure|sync>");
67
+ }
68
+ export async function readAdjustS2sEventMappings(filePath) {
69
+ const text = await readFile(filePath, "utf8");
70
+ const parsed = filePath.endsWith(".json") ? JSON.parse(text) : YAML.parse(text);
71
+ const mappings = Array.isArray(parsed)
72
+ ? parsed
73
+ : parsed && typeof parsed === "object" && Array.isArray(parsed.events)
74
+ ? parsed.events
75
+ : null;
76
+ if (!mappings)
77
+ throw new Error(`${filePath} must be a JSON/YAML array or an object with events[]`);
78
+ return mappings;
79
+ }