@witchpot/gmc 0.6.0 → 0.9.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 +88 -0
- package/README.md +3 -0
- package/package.json +2 -3
- package/skill/gmc-analysis/CHANGELOG.md +197 -0
- package/skill/gmc-analysis/SKILL.md +411 -7
- package/skill/gmc-analysis.manifest.json +2 -2
- package/src/args.js +42 -4
- package/src/constants.js +23 -3
- package/src/help.js +238 -1
- package/src/index.js +1 -0
- package/src/main.js +7 -1
- package/src/main.test.js +130 -0
- package/src/projection.js +21 -0
- package/src/requests.js +474 -2
- package/src/requests.test.js +1065 -0
- package/src/workflows.js +15 -1
- package/src/workflows.test.js +156 -0
- package/scripts/check-release-readiness.mjs +0 -177
- package/scripts/gen-skill-manifest.mjs +0 -151
- package/scripts/smoke-beta.mjs +0 -246
- package/scripts/smoke-pack.mjs +0 -123
package/scripts/smoke-beta.mjs
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
|
|
8
|
-
const packageDir = path.resolve(new URL("..", import.meta.url).pathname);
|
|
9
|
-
const cliBin = path.join(packageDir, "bin", "gmc.mjs");
|
|
10
|
-
const workDir = mkdtempSync(path.join(tmpdir(), "gmc-cli-beta-smoke-"));
|
|
11
|
-
const configPath = path.join(workDir, "config.json");
|
|
12
|
-
const allowApiErrors = process.argv.includes("--allow-api-errors");
|
|
13
|
-
|
|
14
|
-
const smokeCommands = [
|
|
15
|
-
{
|
|
16
|
-
name: "version",
|
|
17
|
-
args: ["--version", "--json"],
|
|
18
|
-
validate: (payload) => requireFields(payload, ["data.name", "data.version"])
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
name: "update",
|
|
22
|
-
args: ["update", "--json"],
|
|
23
|
-
validate: (payload) => requireFields(payload, ["data.currentVersion", "data.commands.npmGlobal"])
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
name: "auth status",
|
|
27
|
-
args: ["auth", "status", "--json"],
|
|
28
|
-
validate: (payload) => requireSuccessEnvelope(payload, ["data.caller", "data.cli"])
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
name: "schema",
|
|
32
|
-
args: ["schema", "--json"],
|
|
33
|
-
validate: (payload) => requireSuccessEnvelope(payload, ["data.version", "data.cliCommands"])
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: "search recent reviewed games",
|
|
37
|
-
args: [
|
|
38
|
-
"games",
|
|
39
|
-
"search",
|
|
40
|
-
"--source",
|
|
41
|
-
"steam",
|
|
42
|
-
"--release-year",
|
|
43
|
-
"2025",
|
|
44
|
-
"--reviews-min",
|
|
45
|
-
"1000",
|
|
46
|
-
"--limit",
|
|
47
|
-
"5",
|
|
48
|
-
"--json"
|
|
49
|
-
],
|
|
50
|
-
validate: (payload) => requireSuccessEnvelope(payload, ["data"])
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: "aggregate recent reviewed games",
|
|
54
|
-
args: [
|
|
55
|
-
"games",
|
|
56
|
-
"aggregate",
|
|
57
|
-
"--source",
|
|
58
|
-
"steam",
|
|
59
|
-
"--release-year",
|
|
60
|
-
"2025",
|
|
61
|
-
"--metric",
|
|
62
|
-
"count,median_reviews",
|
|
63
|
-
"--group-by",
|
|
64
|
-
"release_year",
|
|
65
|
-
"--reviews-min",
|
|
66
|
-
"1000",
|
|
67
|
-
"--json"
|
|
68
|
-
],
|
|
69
|
-
validate: (payload) => requireSuccessEnvelope(payload, ["data"])
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: "benchmark city builder automation",
|
|
73
|
-
args: [
|
|
74
|
-
"games",
|
|
75
|
-
"benchmark",
|
|
76
|
-
"--source",
|
|
77
|
-
"steam",
|
|
78
|
-
"--tags",
|
|
79
|
-
"city_builder,automation",
|
|
80
|
-
"--reviews-min",
|
|
81
|
-
"1000",
|
|
82
|
-
"--preset",
|
|
83
|
-
"summary",
|
|
84
|
-
"--json"
|
|
85
|
-
],
|
|
86
|
-
validate: (payload) => requireComposedEnvelope(payload, ["data.summary"])
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
name: "showcase patterns by tag",
|
|
90
|
-
args: ["showcases", "patterns", "--source", "steam", "--tags", "city_builder,automation", "--json"],
|
|
91
|
-
validate: (payload) => requireComposedEnvelope(payload, ["data.summary"])
|
|
92
|
-
}
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const results = smokeCommands.map(runSmokeCommand);
|
|
97
|
-
const failed = results.filter((result) => result.status === "failed");
|
|
98
|
-
const summary = {
|
|
99
|
-
ok: failed.length === 0,
|
|
100
|
-
apiBaseUrl: process.env.GMC_API_BASE_URL || "https://gamemarketcopilot.com",
|
|
101
|
-
auth: {
|
|
102
|
-
apiKeyConfigured: Boolean(process.env.GMC_API_KEY)
|
|
103
|
-
},
|
|
104
|
-
allowApiErrors,
|
|
105
|
-
commands: results
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
109
|
-
if (failed.length > 0) process.exit(1);
|
|
110
|
-
} finally {
|
|
111
|
-
rmSync(workDir, { recursive: true, force: true });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function runSmokeCommand(command) {
|
|
115
|
-
const result = spawnSync(process.execPath, [cliBin, ...command.args], {
|
|
116
|
-
encoding: "utf8",
|
|
117
|
-
env: {
|
|
118
|
-
...process.env,
|
|
119
|
-
GMC_CONFIG_PATH: configPath
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const payload = parseJson(result.stdout);
|
|
124
|
-
if (!payload.ok) {
|
|
125
|
-
return {
|
|
126
|
-
name: command.name,
|
|
127
|
-
status: "failed",
|
|
128
|
-
exitCode: result.status,
|
|
129
|
-
error: `stdout was not valid JSON: ${payload.error}`
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (payload.value?.error) {
|
|
134
|
-
const stable = isStableError(payload.value.error);
|
|
135
|
-
const status = allowApiErrors && stable ? "api_error_allowed" : "failed";
|
|
136
|
-
return {
|
|
137
|
-
name: command.name,
|
|
138
|
-
status,
|
|
139
|
-
exitCode: result.status,
|
|
140
|
-
error: summarizeError(payload.value.error)
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (result.status !== 0) {
|
|
145
|
-
return {
|
|
146
|
-
name: command.name,
|
|
147
|
-
status: "failed",
|
|
148
|
-
exitCode: result.status,
|
|
149
|
-
error: "command exited non-zero without a structured error envelope"
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const validation = command.validate(payload.value);
|
|
154
|
-
if (!validation.ok) {
|
|
155
|
-
return {
|
|
156
|
-
name: command.name,
|
|
157
|
-
status: "failed",
|
|
158
|
-
exitCode: result.status,
|
|
159
|
-
error: validation.error
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
name: command.name,
|
|
165
|
-
status: "passed",
|
|
166
|
-
exitCode: result.status,
|
|
167
|
-
requestId: payload.value.meta?.requestId,
|
|
168
|
-
upstreamRequestIds: Array.isArray(payload.value.meta?.upstreamRequests)
|
|
169
|
-
? payload.value.meta.upstreamRequests.map((request) => request?.requestId).filter(Boolean)
|
|
170
|
-
: undefined,
|
|
171
|
-
warnings: Array.isArray(payload.value.warnings) ? payload.value.warnings.length : 0,
|
|
172
|
-
hasPagination: Boolean(payload.value.pagination),
|
|
173
|
-
hasEntitlements: Boolean(payload.value.meta?.entitlements),
|
|
174
|
-
hasUpstreamEntitlements: Array.isArray(payload.value.meta?.upstreamRequests)
|
|
175
|
-
? payload.value.meta.upstreamRequests.some((request) => request?.entitlements)
|
|
176
|
-
: undefined
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function parseJson(stdout) {
|
|
181
|
-
try {
|
|
182
|
-
return {
|
|
183
|
-
ok: true,
|
|
184
|
-
value: JSON.parse(stdout)
|
|
185
|
-
};
|
|
186
|
-
} catch (error) {
|
|
187
|
-
return {
|
|
188
|
-
ok: false,
|
|
189
|
-
error: error instanceof Error ? error.message : "unknown parse error"
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function requireSuccessEnvelope(payload, fields) {
|
|
195
|
-
const base = requireFields(payload, ["data", "meta.requestId", ...fields]);
|
|
196
|
-
if (!base.ok) return base;
|
|
197
|
-
return {
|
|
198
|
-
ok: true
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function requireComposedEnvelope(payload, fields) {
|
|
203
|
-
const base = requireFields(payload, ["data", "meta.requestId", "meta.upstreamRequests", ...fields]);
|
|
204
|
-
if (!base.ok) return base;
|
|
205
|
-
if (!Array.isArray(payload.meta.upstreamRequests) || payload.meta.upstreamRequests.length === 0) {
|
|
206
|
-
return {
|
|
207
|
-
ok: false,
|
|
208
|
-
error: "meta.upstreamRequests must include at least one request"
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
return {
|
|
212
|
-
ok: true
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function requireFields(payload, fields) {
|
|
217
|
-
const missing = fields.filter((field) => getPath(payload, field) == null);
|
|
218
|
-
if (missing.length > 0) {
|
|
219
|
-
return {
|
|
220
|
-
ok: false,
|
|
221
|
-
error: `missing fields: ${missing.join(", ")}`
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
return {
|
|
225
|
-
ok: true
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function getPath(value, pathName) {
|
|
230
|
-
return pathName.split(".").reduce((current, part) => current?.[part], value);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function isStableError(error) {
|
|
234
|
-
return Boolean(error && typeof error.code === "string" && typeof error.message === "string");
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function summarizeError(error) {
|
|
238
|
-
return {
|
|
239
|
-
code: error.code,
|
|
240
|
-
message: error.message,
|
|
241
|
-
requestId: error.requestId,
|
|
242
|
-
status: error.status,
|
|
243
|
-
contentType: error.contentType,
|
|
244
|
-
path: error.path
|
|
245
|
-
};
|
|
246
|
-
}
|
package/scripts/smoke-pack.mjs
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
|
-
|
|
8
|
-
const packageDir = path.resolve(new URL("..", import.meta.url).pathname);
|
|
9
|
-
const dryRunOnly = process.argv.includes("--dry-run");
|
|
10
|
-
const workDir = mkdtempSync(path.join(tmpdir(), "gmc-cli-pack-"));
|
|
11
|
-
const npmCache = path.join(workDir, "npm-cache");
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
run("npm", ["pack", "--dry-run"], {
|
|
15
|
-
cwd: packageDir,
|
|
16
|
-
env: {
|
|
17
|
-
npm_config_cache: npmCache
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
if (dryRunOnly) process.exit(0);
|
|
22
|
-
|
|
23
|
-
const packResult = run("npm", ["pack", "--json", "--pack-destination", workDir], {
|
|
24
|
-
cwd: packageDir,
|
|
25
|
-
env: {
|
|
26
|
-
npm_config_cache: npmCache
|
|
27
|
-
},
|
|
28
|
-
capture: true
|
|
29
|
-
});
|
|
30
|
-
const packed = JSON.parse(packResult.stdout);
|
|
31
|
-
const filename = packed?.[0]?.filename;
|
|
32
|
-
if (!filename) throw new Error("npm pack did not return a tarball filename");
|
|
33
|
-
|
|
34
|
-
const projectDir = path.join(workDir, "project");
|
|
35
|
-
run("npm", ["init", "-y"], {
|
|
36
|
-
cwd: workDir,
|
|
37
|
-
env: {
|
|
38
|
-
npm_config_cache: npmCache
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
run("npm", ["install", path.join(workDir, filename)], {
|
|
42
|
-
cwd: workDir,
|
|
43
|
-
env: {
|
|
44
|
-
npm_config_cache: npmCache
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const binName = process.platform === "win32" ? "gmc.cmd" : "gmc";
|
|
49
|
-
const gmcBin = path.join(workDir, "node_modules", ".bin", binName);
|
|
50
|
-
const configPath = path.join(projectDir, "config.json");
|
|
51
|
-
const helpResult = run(gmcBin, ["--help"], {
|
|
52
|
-
env: { GMC_CONFIG_PATH: configPath },
|
|
53
|
-
capture: true
|
|
54
|
-
});
|
|
55
|
-
if (!helpResult.stdout.includes("Game Market Copilot CLI") || !helpResult.stdout.includes("gmc --version")) {
|
|
56
|
-
throw new Error("Packed gmc --help did not print the expected help text");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const configResult = run(gmcBin, ["config", "get", "--json"], {
|
|
60
|
-
env: { GMC_CONFIG_PATH: configPath },
|
|
61
|
-
capture: true
|
|
62
|
-
});
|
|
63
|
-
const configPayload = JSON.parse(configResult.stdout);
|
|
64
|
-
if (configPayload?.data?.configPath !== configPath || configPayload?.data?.authMode !== "anonymous") {
|
|
65
|
-
throw new Error("Packed gmc config get --json did not print the expected config payload");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const skillStatusResult = run(gmcBin, ["skill", "status", "--json"], {
|
|
69
|
-
env: { GMC_CONFIG_PATH: configPath },
|
|
70
|
-
capture: true
|
|
71
|
-
});
|
|
72
|
-
const skillStatus = JSON.parse(skillStatusResult.stdout);
|
|
73
|
-
if (!skillStatus?.data?.packaged?.version) {
|
|
74
|
-
throw new Error("Packed gmc skill status did not report a packaged skill version");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const skillPrintResult = run(gmcBin, ["skill", "print", "--format", "agents-md"], {
|
|
78
|
-
env: { GMC_CONFIG_PATH: configPath },
|
|
79
|
-
capture: true
|
|
80
|
-
});
|
|
81
|
-
if (!skillPrintResult.stdout.includes("GMC Analysis") || !skillPrintResult.stdout.includes("Pitfalls")) {
|
|
82
|
-
throw new Error("Packed gmc skill print did not emit the inlined agents-md section");
|
|
83
|
-
}
|
|
84
|
-
if (!skillPrintResult.stdout.includes("Chart cards") || !skillPrintResult.stdout.includes("gmc-lockup.svg")) {
|
|
85
|
-
throw new Error("Packed gmc skill print did not inline the chart cards reference");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// gmc is shadowed by Oh My Zsh's `git merge --continue` alias, so the
|
|
89
|
-
// package ships alias binaries; make sure npm actually installed them.
|
|
90
|
-
for (const aliasName of ["gmcp", "game-market-copilot"]) {
|
|
91
|
-
const aliasBinName = process.platform === "win32" ? `${aliasName}.cmd` : aliasName;
|
|
92
|
-
const aliasBin = path.join(workDir, "node_modules", ".bin", aliasBinName);
|
|
93
|
-
const aliasResult = run(aliasBin, ["--version"], {
|
|
94
|
-
env: { GMC_CONFIG_PATH: configPath },
|
|
95
|
-
capture: true
|
|
96
|
-
});
|
|
97
|
-
if (!/^gmc \d+\.\d+\.\d+/.test(aliasResult.stdout)) {
|
|
98
|
-
throw new Error(`Packed ${aliasName} --version did not print the CLI version`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} finally {
|
|
102
|
-
rmSync(workDir, { recursive: true, force: true });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function run(command, args, options = {}) {
|
|
106
|
-
const result = spawnSync(command, args, {
|
|
107
|
-
cwd: options.cwd,
|
|
108
|
-
encoding: "utf8",
|
|
109
|
-
env: {
|
|
110
|
-
...process.env,
|
|
111
|
-
npm_config_dry_run: "false",
|
|
112
|
-
...(options.env ?? {})
|
|
113
|
-
},
|
|
114
|
-
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit"
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
if (result.status !== 0) {
|
|
118
|
-
const stderr = result.stderr ? `\n${result.stderr}` : "";
|
|
119
|
-
throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status}${stderr}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return result;
|
|
123
|
-
}
|