@witchpot/gmc 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/CHANGELOG.md +19 -0
- package/README.md +118 -0
- package/bin/gmc.mjs +50 -0
- package/package.json +42 -0
- package/scripts/check-release-readiness.mjs +99 -0
- package/scripts/smoke-beta.mjs +220 -0
- package/scripts/smoke-pack.mjs +75 -0
- package/src/args.js +99 -0
- package/src/browser-login.js +255 -0
- package/src/config.js +320 -0
- package/src/constants.js +66 -0
- package/src/help.js +60 -0
- package/src/main.js +141 -0
- package/src/output.js +3 -0
- package/src/projection.js +163 -0
- package/src/requests.js +600 -0
- package/src/workflows.js +1862 -0
package/src/requests.js
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COMPARE_FOCUSES,
|
|
3
|
+
DEFAULT_BENCHMARK_LIMIT,
|
|
4
|
+
DEFAULT_CAMPAIGN_SIGNALS_LIMIT,
|
|
5
|
+
DEFAULT_COHORT_WORKFLOW_LIMIT,
|
|
6
|
+
DEFAULT_RESOLVE_LIMIT,
|
|
7
|
+
DEFAULT_REVIEW_PATTERNS_LIMIT,
|
|
8
|
+
DEFAULT_REVIEW_PATTERNS_MAX_TOPICS_PER_GAME,
|
|
9
|
+
DEFAULT_REVIEW_PATTERNS_MIN_GAMES,
|
|
10
|
+
DEFAULT_SHOWCASE_FIT_LIMIT,
|
|
11
|
+
EXIT_USAGE,
|
|
12
|
+
MAX_SAMPLE_PAGES,
|
|
13
|
+
MAX_SAMPLE_SIZE
|
|
14
|
+
} from "./constants.js";
|
|
15
|
+
import { matches } from "./args.js";
|
|
16
|
+
import { writeJsonError } from "./output.js";
|
|
17
|
+
|
|
18
|
+
export function buildRequest(command, options, positional) {
|
|
19
|
+
if (matches(command, ["status"])) {
|
|
20
|
+
return {
|
|
21
|
+
path: "/api/v1/status",
|
|
22
|
+
query: {}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (matches(command, ["auth", "status"])) {
|
|
27
|
+
return {
|
|
28
|
+
path: "/api/v1/status",
|
|
29
|
+
query: {}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (matches(command, ["schema"])) {
|
|
34
|
+
return {
|
|
35
|
+
path: "/api/v1/schema",
|
|
36
|
+
query: {}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (matches(command, ["games", "search"])) {
|
|
41
|
+
assertSource(options.source);
|
|
42
|
+
return {
|
|
43
|
+
path: "/api/v1/steam/games",
|
|
44
|
+
query: buildGameSearchQuery(options)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (matches(command, ["games", "resolve"])) {
|
|
49
|
+
assertSource(options.source);
|
|
50
|
+
const query = options.query ?? options.q ?? positional.join(" ");
|
|
51
|
+
assertResolveQuery(query);
|
|
52
|
+
return {
|
|
53
|
+
kind: "resolve",
|
|
54
|
+
source: "steam",
|
|
55
|
+
query: String(query),
|
|
56
|
+
searchRequest: {
|
|
57
|
+
path: "/api/v1/steam/games",
|
|
58
|
+
query: {
|
|
59
|
+
limit: DEFAULT_RESOLVE_LIMIT,
|
|
60
|
+
...buildGameSearchQuery({
|
|
61
|
+
...options,
|
|
62
|
+
query
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (matches(command, ["games", "benchmark"])) {
|
|
70
|
+
assertSource(options.source);
|
|
71
|
+
const sampling = buildSamplingOptions(options, DEFAULT_BENCHMARK_LIMIT);
|
|
72
|
+
const query = {
|
|
73
|
+
sort: "reviews_desc",
|
|
74
|
+
limit: sampling?.pageLimit ?? DEFAULT_BENCHMARK_LIMIT,
|
|
75
|
+
...buildGameSearchQuery(options)
|
|
76
|
+
};
|
|
77
|
+
if (sampling && options.limit == null) query.limit = sampling.pageLimit;
|
|
78
|
+
return {
|
|
79
|
+
kind: "benchmark",
|
|
80
|
+
source: "steam",
|
|
81
|
+
query,
|
|
82
|
+
sampling,
|
|
83
|
+
searchRequest: {
|
|
84
|
+
path: "/api/v1/steam/games",
|
|
85
|
+
query
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (matches(command, ["games", "detail"])) {
|
|
91
|
+
assertSource(options.source);
|
|
92
|
+
const externalId = positional[0];
|
|
93
|
+
assertExternalId(externalId, "gmc games detail --source steam <externalId> --json");
|
|
94
|
+
return {
|
|
95
|
+
path: `/api/v1/steam/games/${encodeURIComponent(externalId)}`,
|
|
96
|
+
query: pickQuery(options, {
|
|
97
|
+
locale: "locale",
|
|
98
|
+
detail: "detail"
|
|
99
|
+
})
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (matches(command, ["games", "analysis"])) {
|
|
104
|
+
assertSource(options.source);
|
|
105
|
+
const externalId = positional[0];
|
|
106
|
+
assertExternalId(externalId, "gmc games analysis --source steam <externalId> --sections reviews,marketing --json");
|
|
107
|
+
return {
|
|
108
|
+
path: `/api/v1/steam/games/${encodeURIComponent(externalId)}/analysis`,
|
|
109
|
+
query: pickQuery(options, {
|
|
110
|
+
sections: "sections",
|
|
111
|
+
locale: "locale"
|
|
112
|
+
})
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (matches(command, ["campaigns", "signals"])) {
|
|
117
|
+
assertSource(options.source);
|
|
118
|
+
const externalId = positional[0];
|
|
119
|
+
assertExternalId(externalId, "gmc campaigns signals --source steam <externalId> --json");
|
|
120
|
+
const limit = assertPositiveIntegerOption(options.limit ?? DEFAULT_CAMPAIGN_SIGNALS_LIMIT, "--limit");
|
|
121
|
+
return {
|
|
122
|
+
kind: "campaignSignals",
|
|
123
|
+
source: "steam",
|
|
124
|
+
externalId,
|
|
125
|
+
limit,
|
|
126
|
+
detailQuery: pickQuery(options, {
|
|
127
|
+
locale: "locale"
|
|
128
|
+
})
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (matches(command, ["games", "compare"])) {
|
|
133
|
+
assertSource(options.source);
|
|
134
|
+
assertCompareExternalIds(positional);
|
|
135
|
+
return {
|
|
136
|
+
kind: "compare",
|
|
137
|
+
source: "steam",
|
|
138
|
+
externalIds: positional,
|
|
139
|
+
focus: assertCompareFocus(options.focus ?? "market"),
|
|
140
|
+
query: pickQuery(options, {
|
|
141
|
+
locale: "locale"
|
|
142
|
+
})
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (matches(command, ["games", "topics"])) {
|
|
147
|
+
assertSource(options.source);
|
|
148
|
+
const externalId = positional[0];
|
|
149
|
+
assertExternalId(externalId, "gmc games topics --source steam <externalId> --json");
|
|
150
|
+
return {
|
|
151
|
+
path: `/api/v1/steam/games/${encodeURIComponent(externalId)}/review-topics`,
|
|
152
|
+
query: pickQuery(options, {
|
|
153
|
+
limit: "limit",
|
|
154
|
+
offset: "offset",
|
|
155
|
+
locale: "locale"
|
|
156
|
+
})
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (matches(command, ["reviews", "patterns"])) {
|
|
161
|
+
assertSource(options.source);
|
|
162
|
+
const isCompare = positional[0] === "compare";
|
|
163
|
+
const minGames = assertPositiveIntegerOption(
|
|
164
|
+
options["min-games"] ?? DEFAULT_REVIEW_PATTERNS_MIN_GAMES,
|
|
165
|
+
"--min-games"
|
|
166
|
+
);
|
|
167
|
+
const maxTopicsPerGame = assertPositiveIntegerOption(
|
|
168
|
+
options["max-topics-per-game"] ?? DEFAULT_REVIEW_PATTERNS_MAX_TOPICS_PER_GAME,
|
|
169
|
+
"--max-topics-per-game"
|
|
170
|
+
);
|
|
171
|
+
const sentiment = options.sentiment == null ? undefined : assertReviewPatternSentiment(options.sentiment);
|
|
172
|
+
const sampling = buildSamplingOptions(options, DEFAULT_REVIEW_PATTERNS_LIMIT);
|
|
173
|
+
const query = {
|
|
174
|
+
sort: "reviews_desc",
|
|
175
|
+
limit: sampling?.pageLimit ?? DEFAULT_REVIEW_PATTERNS_LIMIT,
|
|
176
|
+
...buildGameSearchQuery(options)
|
|
177
|
+
};
|
|
178
|
+
if (sampling && options.limit == null) query.limit = sampling.pageLimit;
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
kind: "reviewPatterns",
|
|
182
|
+
source: "steam",
|
|
183
|
+
query,
|
|
184
|
+
sampling,
|
|
185
|
+
minGames,
|
|
186
|
+
maxTopicsPerGame,
|
|
187
|
+
sentiment,
|
|
188
|
+
topic: options.topic,
|
|
189
|
+
compareBy: isCompare ? String(options.segment ?? "price") : undefined,
|
|
190
|
+
searchRequest: {
|
|
191
|
+
path: "/api/v1/steam/games",
|
|
192
|
+
query
|
|
193
|
+
},
|
|
194
|
+
topicQuery: {
|
|
195
|
+
...pickQuery(options, {
|
|
196
|
+
locale: "locale"
|
|
197
|
+
}),
|
|
198
|
+
limit: maxTopicsPerGame
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (matches(command, ["marketing", "patterns"])) {
|
|
204
|
+
return buildCohortEvidenceRequest({
|
|
205
|
+
workflow: "marketingPatterns",
|
|
206
|
+
evidenceType: "marketing",
|
|
207
|
+
sections: ["marketing", "media", "creators"],
|
|
208
|
+
command,
|
|
209
|
+
options,
|
|
210
|
+
positional
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (matches(command, ["marketing", "timing"])) {
|
|
215
|
+
return buildCohortEvidenceRequest({
|
|
216
|
+
workflow: "marketingTiming",
|
|
217
|
+
evidenceType: "marketingTiming",
|
|
218
|
+
sections: ["marketing"],
|
|
219
|
+
command,
|
|
220
|
+
options,
|
|
221
|
+
positional
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (matches(command, ["creators", "patterns"])) {
|
|
226
|
+
return buildCohortEvidenceRequest({
|
|
227
|
+
workflow: "creatorPatterns",
|
|
228
|
+
evidenceType: "creators",
|
|
229
|
+
sections: ["creators"],
|
|
230
|
+
command,
|
|
231
|
+
options,
|
|
232
|
+
positional
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (matches(command, ["cohort", "trends"])) {
|
|
237
|
+
return buildCohortEvidenceRequest({
|
|
238
|
+
workflow: "cohortTrends",
|
|
239
|
+
evidenceType: "trends",
|
|
240
|
+
sections: [],
|
|
241
|
+
command,
|
|
242
|
+
options,
|
|
243
|
+
positional
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (matches(command, ["cohort", "pricing"])) {
|
|
248
|
+
return buildCohortEvidenceRequest({
|
|
249
|
+
workflow: "cohortPricing",
|
|
250
|
+
evidenceType: "pricing",
|
|
251
|
+
sections: [],
|
|
252
|
+
command,
|
|
253
|
+
options,
|
|
254
|
+
positional
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (matches(command, ["cohort", "brief"])) {
|
|
259
|
+
return buildCohortEvidenceRequest({
|
|
260
|
+
workflow: "cohortBrief",
|
|
261
|
+
evidenceType: "brief",
|
|
262
|
+
sections: ["reviews", "marketing", "creators", "media", "showcases"],
|
|
263
|
+
command,
|
|
264
|
+
options,
|
|
265
|
+
positional
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (matches(command, ["cohort", "compare"])) {
|
|
270
|
+
return buildCohortEvidenceRequest({
|
|
271
|
+
workflow: "cohortCompare",
|
|
272
|
+
evidenceType: "trends",
|
|
273
|
+
sections: [],
|
|
274
|
+
command,
|
|
275
|
+
options,
|
|
276
|
+
positional
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (matches(command, ["showcases", "search"])) {
|
|
281
|
+
const deadlineBefore = options["deadline-before"];
|
|
282
|
+
if (deadlineBefore != null) assertDate(deadlineBefore, "--deadline-before");
|
|
283
|
+
return {
|
|
284
|
+
path: "/api/v1/showcases",
|
|
285
|
+
query: buildShowcaseSearchQuery(options),
|
|
286
|
+
transform: deadlineBefore
|
|
287
|
+
? (payload) => filterShowcasesByDeadline(payload, String(deadlineBefore))
|
|
288
|
+
: undefined
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (matches(command, ["showcases", "patterns"])) {
|
|
293
|
+
return buildCohortEvidenceRequest({
|
|
294
|
+
workflow: "showcasePatterns",
|
|
295
|
+
evidenceType: "showcases",
|
|
296
|
+
sections: ["showcases"],
|
|
297
|
+
command,
|
|
298
|
+
options,
|
|
299
|
+
positional
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (matches(command, ["showcases", "fit"])) {
|
|
304
|
+
assertSource(options.source);
|
|
305
|
+
const externalId = positional[0];
|
|
306
|
+
assertExternalId(externalId, "gmc showcases fit --source steam <externalId> --json");
|
|
307
|
+
const deadlineBefore = options["deadline-before"];
|
|
308
|
+
if (deadlineBefore != null) assertDate(deadlineBefore, "--deadline-before");
|
|
309
|
+
return {
|
|
310
|
+
kind: "showcaseFit",
|
|
311
|
+
source: "steam",
|
|
312
|
+
externalId,
|
|
313
|
+
tagsOverride: options.tags ? parseCsvString(options.tags) : undefined,
|
|
314
|
+
deadlineBefore: deadlineBefore ? String(deadlineBefore) : undefined,
|
|
315
|
+
detailQuery: pickQuery(options, {
|
|
316
|
+
locale: "locale"
|
|
317
|
+
}),
|
|
318
|
+
showcaseQuery: {
|
|
319
|
+
status: "open",
|
|
320
|
+
sort: "deadline_asc",
|
|
321
|
+
limit: DEFAULT_SHOWCASE_FIT_LIMIT,
|
|
322
|
+
...buildShowcaseSearchQuery(options)
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function buildCohortEvidenceRequest({ workflow, evidenceType, sections, options }) {
|
|
331
|
+
assertSource(options.source);
|
|
332
|
+
const sampling = buildSamplingOptions(options, DEFAULT_COHORT_WORKFLOW_LIMIT);
|
|
333
|
+
const query = {
|
|
334
|
+
sort: "reviews_desc",
|
|
335
|
+
limit: sampling?.pageLimit ?? DEFAULT_COHORT_WORKFLOW_LIMIT,
|
|
336
|
+
...buildGameSearchQuery(options)
|
|
337
|
+
};
|
|
338
|
+
if (sampling && options.limit == null) query.limit = sampling.pageLimit;
|
|
339
|
+
return {
|
|
340
|
+
kind: "cohortEvidence",
|
|
341
|
+
source: "steam",
|
|
342
|
+
workflow,
|
|
343
|
+
evidenceType,
|
|
344
|
+
sections,
|
|
345
|
+
query,
|
|
346
|
+
sampling,
|
|
347
|
+
searchRequest: {
|
|
348
|
+
path: "/api/v1/steam/games",
|
|
349
|
+
query
|
|
350
|
+
},
|
|
351
|
+
analysisQuery: sections.length
|
|
352
|
+
? {
|
|
353
|
+
...pickQuery(options, {
|
|
354
|
+
locale: "locale"
|
|
355
|
+
}),
|
|
356
|
+
sections: sections.join(",")
|
|
357
|
+
}
|
|
358
|
+
: undefined
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function createRequestUrl(baseUrl, request) {
|
|
363
|
+
const url = new URL(request.path, baseUrl);
|
|
364
|
+
for (const [key, value] of Object.entries(request.query)) {
|
|
365
|
+
if (value != null && value !== "") url.searchParams.set(key, String(value));
|
|
366
|
+
}
|
|
367
|
+
return url;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function buildRequestHeaders(apiKey) {
|
|
371
|
+
return {
|
|
372
|
+
accept: "application/json",
|
|
373
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {})
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function pickQuery(options, mapping) {
|
|
378
|
+
const query = {};
|
|
379
|
+
for (const [optionName, queryName] of Object.entries(mapping)) {
|
|
380
|
+
if (options[optionName] != null) query[queryName] = options[optionName];
|
|
381
|
+
}
|
|
382
|
+
return query;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function buildGameSearchQuery(options) {
|
|
386
|
+
const query = pickQuery(options, {
|
|
387
|
+
query: "q",
|
|
388
|
+
tags: "tags",
|
|
389
|
+
"tag-mode": "tagMode",
|
|
390
|
+
"release-from": "releaseFrom",
|
|
391
|
+
"release-to": "releaseTo",
|
|
392
|
+
"reviews-min": "reviewsMin",
|
|
393
|
+
"reviews-max": "reviewsMax",
|
|
394
|
+
"rating-min": "ratingMin",
|
|
395
|
+
"price-min": "priceMin",
|
|
396
|
+
"price-max": "priceMax",
|
|
397
|
+
sort: "sort",
|
|
398
|
+
limit: "limit",
|
|
399
|
+
offset: "offset",
|
|
400
|
+
locale: "locale"
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (query.tags) query.tags = normalizeTagSlugCsv(query.tags);
|
|
404
|
+
return query;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function buildShowcaseSearchQuery(options) {
|
|
408
|
+
return pickQuery(options, {
|
|
409
|
+
query: "q",
|
|
410
|
+
tags: "tags",
|
|
411
|
+
"tag-mode": "tagMode",
|
|
412
|
+
status: "status",
|
|
413
|
+
format: "format",
|
|
414
|
+
country: "country",
|
|
415
|
+
sort: "sort",
|
|
416
|
+
limit: "limit",
|
|
417
|
+
offset: "offset",
|
|
418
|
+
locale: "locale"
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function assertSource(source = "steam") {
|
|
423
|
+
if (source !== "steam") {
|
|
424
|
+
writeJsonError({
|
|
425
|
+
code: "CLI_USAGE_ERROR",
|
|
426
|
+
message: "Only --source steam is supported by the current API"
|
|
427
|
+
});
|
|
428
|
+
process.exit(EXIT_USAGE);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function assertDate(value, optionName) {
|
|
433
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(value))) {
|
|
434
|
+
writeJsonError({
|
|
435
|
+
code: "CLI_USAGE_ERROR",
|
|
436
|
+
message: `${optionName} must use YYYY-MM-DD`
|
|
437
|
+
});
|
|
438
|
+
process.exit(EXIT_USAGE);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function assertPositiveIntegerOption(value, optionName) {
|
|
443
|
+
const number = Number(value);
|
|
444
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
445
|
+
writeJsonError({
|
|
446
|
+
code: "CLI_USAGE_ERROR",
|
|
447
|
+
message: `${optionName} must be a positive integer`
|
|
448
|
+
});
|
|
449
|
+
process.exit(EXIT_USAGE);
|
|
450
|
+
}
|
|
451
|
+
return number;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function buildSamplingOptions(options, defaultLimit) {
|
|
455
|
+
const sampleSize = options["sample-size"];
|
|
456
|
+
const samplePages = options["sample-pages"];
|
|
457
|
+
if (sampleSize != null && samplePages != null) {
|
|
458
|
+
writeJsonError({
|
|
459
|
+
code: "CLI_USAGE_ERROR",
|
|
460
|
+
message: "Use either --sample-size or --sample-pages, not both"
|
|
461
|
+
});
|
|
462
|
+
process.exit(EXIT_USAGE);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (sampleSize == null && samplePages == null) return undefined;
|
|
466
|
+
|
|
467
|
+
const pageLimit = assertPositiveIntegerOption(
|
|
468
|
+
options.limit ?? (sampleSize == null ? defaultLimit : Math.min(Number(sampleSize), 100)),
|
|
469
|
+
"--limit"
|
|
470
|
+
);
|
|
471
|
+
if (pageLimit > 100) {
|
|
472
|
+
writeJsonError({
|
|
473
|
+
code: "CLI_USAGE_ERROR",
|
|
474
|
+
message: "--limit must be 100 or less for sampled workflows"
|
|
475
|
+
});
|
|
476
|
+
process.exit(EXIT_USAGE);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (sampleSize != null) {
|
|
480
|
+
const requestedSize = assertBoundedPositiveIntegerOption(sampleSize, "--sample-size", MAX_SAMPLE_SIZE);
|
|
481
|
+
return {
|
|
482
|
+
mode: "sample-size",
|
|
483
|
+
requestedSize,
|
|
484
|
+
pageLimit
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const requestedPages = assertBoundedPositiveIntegerOption(samplePages, "--sample-pages", MAX_SAMPLE_PAGES);
|
|
489
|
+
return {
|
|
490
|
+
mode: "sample-pages",
|
|
491
|
+
requestedPages,
|
|
492
|
+
pageLimit
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function assertBoundedPositiveIntegerOption(value, optionName, maxValue) {
|
|
497
|
+
const number = assertPositiveIntegerOption(value, optionName);
|
|
498
|
+
if (number > maxValue) {
|
|
499
|
+
writeJsonError({
|
|
500
|
+
code: "CLI_USAGE_ERROR",
|
|
501
|
+
message: `${optionName} must be ${maxValue} or less`
|
|
502
|
+
});
|
|
503
|
+
process.exit(EXIT_USAGE);
|
|
504
|
+
}
|
|
505
|
+
return number;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function assertReviewPatternSentiment(value) {
|
|
509
|
+
const sentiment = String(value);
|
|
510
|
+
if (!["positive", "negative", "mixed"].includes(sentiment)) {
|
|
511
|
+
writeJsonError({
|
|
512
|
+
code: "CLI_USAGE_ERROR",
|
|
513
|
+
message: "--sentiment must be one of: positive, negative, mixed"
|
|
514
|
+
});
|
|
515
|
+
process.exit(EXIT_USAGE);
|
|
516
|
+
}
|
|
517
|
+
return sentiment;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function assertExternalId(value, usage) {
|
|
521
|
+
if (!value) {
|
|
522
|
+
writeJsonError({
|
|
523
|
+
code: "CLI_USAGE_ERROR",
|
|
524
|
+
message: `Missing externalId. Usage: ${usage}`
|
|
525
|
+
});
|
|
526
|
+
process.exit(EXIT_USAGE);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function assertResolveQuery(value) {
|
|
531
|
+
if (value == null || String(value).trim() === "") {
|
|
532
|
+
writeJsonError({
|
|
533
|
+
code: "CLI_USAGE_ERROR",
|
|
534
|
+
message: "Missing query. Usage: gmc games resolve --source steam --query <title> --json"
|
|
535
|
+
});
|
|
536
|
+
process.exit(EXIT_USAGE);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function assertCompareExternalIds(values) {
|
|
541
|
+
if (values.length < 2) {
|
|
542
|
+
writeJsonError({
|
|
543
|
+
code: "CLI_USAGE_ERROR",
|
|
544
|
+
message: "Missing comparison targets. Usage: gmc games compare --source steam <externalId> <externalId...> --json"
|
|
545
|
+
});
|
|
546
|
+
process.exit(EXIT_USAGE);
|
|
547
|
+
}
|
|
548
|
+
if (values.length > 5) {
|
|
549
|
+
writeJsonError({
|
|
550
|
+
code: "CLI_USAGE_ERROR",
|
|
551
|
+
message: "gmc games compare accepts at most 5 external IDs"
|
|
552
|
+
});
|
|
553
|
+
process.exit(EXIT_USAGE);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function assertCompareFocus(value) {
|
|
558
|
+
if (!COMPARE_FOCUSES.includes(value)) {
|
|
559
|
+
writeJsonError({
|
|
560
|
+
code: "CLI_USAGE_ERROR",
|
|
561
|
+
message: `--focus must be one of: ${COMPARE_FOCUSES.join(", ")}`
|
|
562
|
+
});
|
|
563
|
+
process.exit(EXIT_USAGE);
|
|
564
|
+
}
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function parseCsvString(value) {
|
|
569
|
+
return String(value)
|
|
570
|
+
.split(",")
|
|
571
|
+
.map((part) => part.trim())
|
|
572
|
+
.filter(Boolean);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function normalizeTagSlugCsv(value) {
|
|
576
|
+
return parseCsvString(value)
|
|
577
|
+
.map((tag) => tag
|
|
578
|
+
.toLowerCase()
|
|
579
|
+
.replace(/['’]/g, "")
|
|
580
|
+
.replace(/&/g, " and ")
|
|
581
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
582
|
+
.replace(/^_+|_+$/g, ""))
|
|
583
|
+
.filter(Boolean)
|
|
584
|
+
.join(",");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function filterShowcasesByDeadline(payload, deadlineBefore) {
|
|
588
|
+
if (!payload || !Array.isArray(payload.data)) return payload;
|
|
589
|
+
return {
|
|
590
|
+
...payload,
|
|
591
|
+
data: payload.data.filter((showcase) => {
|
|
592
|
+
if (!showcase || typeof showcase !== "object" || !showcase.deadline) return false;
|
|
593
|
+
return String(showcase.deadline) <= deadlineBefore;
|
|
594
|
+
}),
|
|
595
|
+
warnings: [
|
|
596
|
+
...(payload.warnings ?? []),
|
|
597
|
+
"deadline-before was applied client-side by the gmc CLI"
|
|
598
|
+
]
|
|
599
|
+
};
|
|
600
|
+
}
|