@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.
@@ -0,0 +1,66 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+
4
+ export const EXIT_UNEXPECTED = 1;
5
+ export const EXIT_USAGE = 2;
6
+ export const EXIT_CONFIG = 3;
7
+ export const EXIT_API = 4;
8
+
9
+ export const CLI_PACKAGE_NAME = "@witchpot/gmc";
10
+ export const CLI_BETA_PACKAGE_NAME = "@witchpot/gmc";
11
+ export const CLI_VERSION = "0.1.0";
12
+
13
+ export const DEFAULT_API_BASE_URL = "https://gamemarketcopilot.com";
14
+ export const DEFAULT_CONFIG_PATH = path.join(homedir(), ".config", "gmc", "config.json");
15
+
16
+ export const SUPPORTED_BETA_COMMANDS = [
17
+ "gmc status --json",
18
+ "gmc auth status --json",
19
+ "gmc schema --json",
20
+ "gmc --version",
21
+ "gmc update --json",
22
+ "gmc games search --source steam --query <text> --json",
23
+ "gmc games resolve --source steam --query <title> --json",
24
+ "gmc games detail --source steam <externalId> --json",
25
+ "gmc games analysis --source steam <externalId> --sections reviews,marketing --json",
26
+ "gmc campaigns signals --source steam <externalId> --json",
27
+ "gmc games topics --source steam <externalId> --json",
28
+ "gmc games compare --source steam <externalId> <externalId...> --focus market --json",
29
+ "gmc games benchmark --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json",
30
+ "gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json",
31
+ "gmc showcases search --query <text> --json",
32
+ "gmc showcases fit --source steam <externalId> --deadline-before <YYYY-MM-DD> --json"
33
+ ];
34
+
35
+ export const PROVISIONAL_BETA_COMMANDS = [
36
+ "gmc reviews patterns compare --source steam --tags <tag_slug[,tag_slug...]> --segment price --json",
37
+ "gmc marketing patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json",
38
+ "gmc marketing timing --source steam --tags <tag_slug[,tag_slug...]> --json",
39
+ "gmc creators patterns --source steam --tags <tag_slug[,tag_slug...]> --json",
40
+ "gmc cohort trends --source steam --tags <tag_slug[,tag_slug...]> --json",
41
+ "gmc cohort pricing --source steam --tags <tag_slug[,tag_slug...]> --json",
42
+ "gmc cohort compare --source steam --tags <tag_slug[,tag_slug...]> --json",
43
+ "gmc cohort brief --source steam --tags <tag_slug[,tag_slug...]> --json",
44
+ "gmc showcases patterns --source steam --tags <tag_slug[,tag_slug...]> --json"
45
+ ];
46
+
47
+ export const COMPARE_FOCUSES = ["overview", "market", "reviews", "marketing", "audience", "showcases"];
48
+ export const COMPARE_FOCUS_SECTIONS = {
49
+ overview: [],
50
+ market: [],
51
+ reviews: ["reviews"],
52
+ marketing: ["marketing", "creators", "media"],
53
+ audience: ["reviews", "coverage", "creators"],
54
+ showcases: ["showcases"]
55
+ };
56
+
57
+ export const DEFAULT_BENCHMARK_LIMIT = "20";
58
+ export const DEFAULT_CAMPAIGN_SIGNALS_LIMIT = "30";
59
+ export const DEFAULT_COHORT_WORKFLOW_LIMIT = "20";
60
+ export const DEFAULT_RESOLVE_LIMIT = "10";
61
+ export const DEFAULT_REVIEW_PATTERNS_LIMIT = "20";
62
+ export const DEFAULT_REVIEW_PATTERNS_MAX_TOPICS_PER_GAME = "10";
63
+ export const DEFAULT_REVIEW_PATTERNS_MIN_GAMES = "3";
64
+ export const DEFAULT_SHOWCASE_FIT_LIMIT = "25";
65
+ export const MAX_SAMPLE_PAGES = 10;
66
+ export const MAX_SAMPLE_SIZE = 500;
package/src/help.js ADDED
@@ -0,0 +1,60 @@
1
+ import {
2
+ CLI_VERSION,
3
+ PROVISIONAL_BETA_COMMANDS,
4
+ SUPPORTED_BETA_COMMANDS
5
+ } from "./constants.js";
6
+
7
+ export function printHelp() {
8
+ process.stdout.write(`Game Market Copilot CLI
9
+
10
+ Version:
11
+ ${CLI_VERSION}
12
+
13
+ Usage:
14
+ gmc --version
15
+ gmc status --json
16
+ gmc auth status --json
17
+ gmc login
18
+ gmc login --no-open
19
+ gmc login --api-key <key>
20
+ gmc logout
21
+ gmc update --json
22
+ gmc config get --json
23
+ gmc config set apiBaseUrl <url>
24
+ gmc schema --json
25
+ gmc games search --source steam --query <text> --json
26
+ gmc games resolve --source steam --query <title> --json
27
+ gmc games benchmark --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json
28
+ gmc games benchmark --source steam --tags <tag_slug[,tag_slug...]> --sample-size <number> --json
29
+ gmc games detail --source steam <externalId> --json
30
+ gmc games detail --source steam <externalId> --fields title,snapshot.rating,snapshot.reviews --json
31
+ gmc games analysis --source steam <externalId> --sections reviews,marketing --json
32
+ gmc campaigns signals --source steam <externalId> --json
33
+ gmc games compare --source steam <externalId> <externalId...> --focus market --json
34
+ gmc games topics --source steam <externalId> --json
35
+ gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json
36
+ gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --sample-size <number> --preset summary --json
37
+ gmc marketing patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json
38
+ gmc marketing timing --source steam --tags <tag_slug[,tag_slug...]> --json
39
+ gmc creators patterns --source steam --tags <tag_slug[,tag_slug...]> --json
40
+ gmc cohort trends --source steam --tags <tag_slug[,tag_slug...]> --json
41
+ gmc cohort pricing --source steam --tags <tag_slug[,tag_slug...]> --json
42
+ gmc cohort compare --source steam --tags <tag_slug[,tag_slug...]> --json
43
+ gmc cohort brief --source steam --tags <tag_slug[,tag_slug...]> --json
44
+ gmc showcases search --query <text> --json
45
+ gmc showcases fit --source steam <externalId> --deadline-before <YYYY-MM-DD> --json
46
+ gmc showcases patterns --source steam --tags <tag_slug[,tag_slug...]> --json
47
+
48
+ Supported beta commands:
49
+ ${SUPPORTED_BETA_COMMANDS.map((command) => ` ${command}`).join("\n")}
50
+
51
+ Provisional beta commands:
52
+ ${PROVISIONAL_BETA_COMMANDS.map((command) => ` ${command}`).join("\n")}
53
+
54
+ Environment:
55
+ GMC_API_BASE_URL Defaults to https://gamemarketcopilot.com
56
+ GMC_API_KEY Workspace API key, sent only as an Authorization header
57
+ GMC_CLI_CREDENTIAL Browser-login CLI credential, sent only as an Authorization header
58
+ GMC_CONFIG_PATH Optional JSON config with apiBaseUrl and apiKey
59
+ `);
60
+ }
package/src/main.js ADDED
@@ -0,0 +1,141 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import {
3
+ CLI_PACKAGE_NAME,
4
+ CLI_VERSION,
5
+ EXIT_API,
6
+ EXIT_CONFIG,
7
+ EXIT_UNEXPECTED,
8
+ EXIT_USAGE
9
+ } from "./constants.js";
10
+ import { matches, parseArgs } from "./args.js";
11
+ import { handleLocalCommand, loadConfig, parseBaseUrl } from "./config.js";
12
+ import { printHelp } from "./help.js";
13
+ import { writeJsonError } from "./output.js";
14
+ import { applyOutputProjection } from "./projection.js";
15
+ import { buildRequest } from "./requests.js";
16
+ import { executeRequestPlan } from "./workflows.js";
17
+
18
+ export async function main(argv = process.argv.slice(2)) {
19
+ const { command, options, positional } = parseArgs(argv);
20
+
21
+ if (options.version || matches(command, ["version"])) {
22
+ if (options.json) {
23
+ process.stdout.write(`${JSON.stringify({
24
+ data: {
25
+ name: CLI_PACKAGE_NAME,
26
+ version: CLI_VERSION
27
+ }
28
+ }, null, 2)}\n`);
29
+ } else {
30
+ process.stdout.write(`gmc ${CLI_VERSION}\n`);
31
+ }
32
+ process.exit(0);
33
+ }
34
+
35
+ if (options.help || (command.length === 0 && positional.length === 0)) {
36
+ printHelp();
37
+ process.exit(0);
38
+ }
39
+
40
+ const config = loadConfig();
41
+ const localOutput = await handleLocalCommand(command, options, positional, config);
42
+ if (localOutput) {
43
+ process.stdout.write(`${JSON.stringify(applyOutputProjection(localOutput, {
44
+ fields: options.fields,
45
+ preset: options.preset
46
+ }), null, 2)}\n`);
47
+ process.exit(0);
48
+ }
49
+
50
+ const baseUrl = parseBaseUrl(config.apiBaseUrl);
51
+ const apiKey = config.apiKey;
52
+ const request = buildRequest(command, options, positional);
53
+ if (!request) {
54
+ writeJsonError({
55
+ code: "CLI_USAGE_ERROR",
56
+ message: `Unknown command: ${[...command, ...positional].join(" ")}`
57
+ });
58
+ process.exit(EXIT_USAGE);
59
+ }
60
+
61
+ if (request.requiresApiKey && !apiKey) {
62
+ writeJsonError({
63
+ code: "CLI_MISSING_CONFIG",
64
+ message: "GMC_API_KEY or GMC_CLI_CREDENTIAL is required for this command"
65
+ });
66
+ process.exit(EXIT_CONFIG);
67
+ }
68
+
69
+ const response = await executeRequestPlan(baseUrl, request, { apiKey });
70
+ const payload = matches(command, ["auth", "status"])
71
+ ? enrichAuthStatusOutput(response, config)
72
+ : response;
73
+ const output = applyOutputProjection(payload, {
74
+ fields: options.fields,
75
+ preset: options.preset,
76
+ request
77
+ });
78
+
79
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
80
+ }
81
+
82
+ export function runCliEntryPoint() {
83
+ main().catch((error) => {
84
+ writeJsonError({
85
+ code: "CLI_UNEXPECTED_ERROR",
86
+ message: error instanceof Error ? error.message : "Unexpected CLI failure"
87
+ });
88
+ process.exit(EXIT_UNEXPECTED);
89
+ });
90
+ }
91
+
92
+ export function isCliEntryPoint(importMetaUrl, argvEntry = process.argv[1]) {
93
+ return Boolean(argvEntry && importMetaUrl === pathToFileURL(argvEntry).href);
94
+ }
95
+
96
+ export function enrichAuthStatusOutput(payload, config) {
97
+ if (!payload || typeof payload !== "object" || !payload.data || typeof payload.data !== "object") {
98
+ return payload;
99
+ }
100
+
101
+ return {
102
+ ...payload,
103
+ data: {
104
+ ...payload.data,
105
+ cli: buildCliAuthStatus(config)
106
+ }
107
+ };
108
+ }
109
+
110
+ export function buildCliAuthStatus(config) {
111
+ const authMode = getCliCredentialAuthMode(config);
112
+ return {
113
+ apiBaseUrl: config.apiBaseUrl,
114
+ configPath: config.configPath,
115
+ authMode,
116
+ credential: config.apiKey
117
+ ? {
118
+ type: authMode,
119
+ prefix: getApiKeyPrefix(config.apiKey)
120
+ }
121
+ : {
122
+ type: "none"
123
+ },
124
+ sources: config.sources
125
+ };
126
+ }
127
+
128
+ function getCliCredentialAuthMode(config) {
129
+ if (!config.apiKey) return "anonymous";
130
+ if (config.authMode === "cli_credential" || config.fileConfig?.authMode === "cli_credential") {
131
+ return "cli_credential";
132
+ }
133
+ if (String(config.apiKey).startsWith("gmcc_")) return "cli_credential";
134
+ return "api_key";
135
+ }
136
+
137
+ function getApiKeyPrefix(apiKey) {
138
+ return String(apiKey).slice(0, 12);
139
+ }
140
+
141
+ export { EXIT_API, EXIT_CONFIG, EXIT_UNEXPECTED, EXIT_USAGE };
package/src/output.js ADDED
@@ -0,0 +1,3 @@
1
+ export function writeJsonError(error) {
2
+ process.stdout.write(`${JSON.stringify({ error }, null, 2)}\n`);
3
+ }
@@ -0,0 +1,163 @@
1
+ import { EXIT_USAGE } from "./constants.js";
2
+ import { writeJsonError } from "./output.js";
3
+
4
+ export function applyFieldsProjection(payload, rawFields, label = "fields") {
5
+ if (rawFields == null || rawFields === "") return payload;
6
+
7
+ const fields = parseFields(rawFields);
8
+ if (fields.length === 0) {
9
+ writeJsonError({
10
+ code: "CLI_USAGE_ERROR",
11
+ message: "--fields must include at least one field"
12
+ });
13
+ process.exit(EXIT_USAGE);
14
+ }
15
+
16
+ if (!payload || typeof payload !== "object" || !("data" in payload)) {
17
+ return projectValue(payload, fields);
18
+ }
19
+
20
+ const projection = projectValueWithMatches(payload.data, fields);
21
+ const missingFields = label === "fields"
22
+ ? fields.filter((field) => {
23
+ const pathParts = field.split(".").map((part) => part.trim()).filter(Boolean);
24
+ const normalizedPathParts = normalizeDataPath(pathParts);
25
+ return normalizedPathParts != null && !projection.matchedFields.has(field);
26
+ })
27
+ : [];
28
+
29
+ return {
30
+ ...payload,
31
+ data: projection.value,
32
+ warnings: [
33
+ ...(Array.isArray(payload.warnings) ? payload.warnings : []),
34
+ `${label} was applied client-side by the gmc CLI: ${fields.join(",")}`,
35
+ ...(missingFields.length
36
+ ? [`${label} were not found in data by the gmc CLI: ${missingFields.join(",")}`]
37
+ : [])
38
+ ]
39
+ };
40
+ }
41
+
42
+ export function applyOutputProjection(payload, { fields, preset, request } = {}) {
43
+ if (fields != null && fields !== "") return applyFieldsProjection(payload, fields);
44
+ if (preset == null || preset === "") return payload;
45
+
46
+ const presetFields = resolvePresetFields(preset, request);
47
+ return applyFieldsProjection(payload, presetFields, `preset ${preset}`);
48
+ }
49
+
50
+ function resolvePresetFields(preset, request) {
51
+ if (preset !== "summary") {
52
+ writeJsonError({
53
+ code: "CLI_USAGE_ERROR",
54
+ message: "--preset must be one of: summary"
55
+ });
56
+ process.exit(EXIT_USAGE);
57
+ }
58
+
59
+ if (request?.kind === "reviewPatterns") return "cohort,summary,patterns";
60
+ if (request?.kind === "benchmark") return "summary,sampledSummary";
61
+ if (request?.kind === "cohortEvidence") return "cohort,summary,patterns,segments";
62
+
63
+ writeJsonError({
64
+ code: "CLI_USAGE_ERROR",
65
+ message: "--preset summary is supported for gmc reviews patterns, gmc games benchmark, and gmc cohort/pattern workflows"
66
+ });
67
+ process.exit(EXIT_USAGE);
68
+ }
69
+
70
+ function parseFields(rawFields) {
71
+ return String(rawFields)
72
+ .split(",")
73
+ .map((field) => field.trim())
74
+ .filter(Boolean);
75
+ }
76
+
77
+ function projectValue(value, fields) {
78
+ if (Array.isArray(value)) {
79
+ return value.map((item) => projectObject(item, fields));
80
+ }
81
+ return projectObject(value, fields);
82
+ }
83
+
84
+ function projectValueWithMatches(value, fields) {
85
+ const matchedFields = new Set();
86
+ const projected = Array.isArray(value)
87
+ ? value.map((item) => projectObject(item, fields, matchedFields))
88
+ : projectObject(value, fields, matchedFields);
89
+
90
+ return {
91
+ value: projected,
92
+ matchedFields
93
+ };
94
+ }
95
+
96
+ function projectObject(value, fields, matchedFields) {
97
+ if (!value || typeof value !== "object") return value;
98
+
99
+ const projected = {};
100
+ for (const field of fields) {
101
+ const pathParts = field.split(".").map((part) => part.trim()).filter(Boolean);
102
+ const normalizedPathParts = normalizeDataPath(pathParts);
103
+ if (normalizedPathParts == null) continue;
104
+ if (normalizedPathParts.length === 0) return value;
105
+
106
+ const fieldValue = projectPath(value, normalizedPathParts);
107
+ if (fieldValue !== undefined) {
108
+ matchedFields?.add(field);
109
+ mergeProjected(projected, fieldValue);
110
+ }
111
+ }
112
+ return projected;
113
+ }
114
+
115
+ function normalizeDataPath(pathParts) {
116
+ if (pathParts.length === 0) return null;
117
+ if (!pathParts.every(isSafePathPart)) return null;
118
+ if (pathParts[0] === "data") return pathParts.slice(1);
119
+ if (["error", "meta", "pagination", "warnings"].includes(pathParts[0])) return null;
120
+ return pathParts;
121
+ }
122
+
123
+ function isSafePathPart(part) {
124
+ return part !== "__proto__" && part !== "prototype" && part !== "constructor";
125
+ }
126
+
127
+ function projectPath(value, pathParts) {
128
+ if (value == null) return undefined;
129
+ if (pathParts.length === 0) return value;
130
+
131
+ if (Array.isArray(value)) {
132
+ const items = value
133
+ .map((item) => projectPath(item, pathParts))
134
+ return items.some((item) => item !== undefined) ? items : undefined;
135
+ }
136
+
137
+ if (typeof value !== "object") return undefined;
138
+ const [head, ...tail] = pathParts;
139
+ const child = projectPath(value[head], tail);
140
+ return child === undefined ? undefined : { [head]: child };
141
+ }
142
+
143
+ function mergeProjected(target, source) {
144
+ for (const [key, value] of Object.entries(source)) {
145
+ target[key] = mergeValue(target[key], value);
146
+ }
147
+ return target;
148
+ }
149
+
150
+ function mergeValue(existing, incoming) {
151
+ if (incoming === undefined) return existing;
152
+ if (Array.isArray(existing) && Array.isArray(incoming)) {
153
+ return incoming.map((item, index) => mergeValue(existing[index], item));
154
+ }
155
+ if (isPlainObject(existing) && isPlainObject(incoming)) {
156
+ return mergeProjected({ ...existing }, incoming);
157
+ }
158
+ return incoming;
159
+ }
160
+
161
+ function isPlainObject(value) {
162
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
163
+ }