@witchpot/gmc 0.8.0 → 0.10.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 +52 -0
- package/package.json +1 -2
- package/skill/gmc-analysis/CHANGELOG.md +25 -0
- package/skill/gmc-analysis/SKILL.md +42 -3
- package/skill/gmc-analysis.manifest.json +2 -2
- package/src/args.js +9 -1
- package/src/constants.js +13 -2
- package/src/help.js +99 -0
- package/src/main.js +7 -1
- package/src/main.test.js +130 -0
- package/src/projection.js +21 -0
- package/src/requests.js +234 -0
- package/src/requests.test.js +369 -3
- 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
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
|
|
7
|
-
const packageDir = path.resolve(new URL("..", import.meta.url).pathname);
|
|
8
|
-
const packageJsonPath = path.join(packageDir, "package.json");
|
|
9
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
10
|
-
|
|
11
|
-
const requiredFiles = [
|
|
12
|
-
"bin/gmc.mjs",
|
|
13
|
-
"src/index.js",
|
|
14
|
-
"src/main.js",
|
|
15
|
-
"src/requests.js",
|
|
16
|
-
"src/workflows.js",
|
|
17
|
-
"skill/gmc-analysis/SKILL.md",
|
|
18
|
-
"skill/gmc-analysis/references/recipes.md",
|
|
19
|
-
"skill/gmc-analysis/references/pitfalls.md",
|
|
20
|
-
"skill/gmc-analysis/references/validity.md",
|
|
21
|
-
"skill/gmc-analysis/references/quota.md",
|
|
22
|
-
"skill/gmc-analysis/references/charts.md",
|
|
23
|
-
"skill/gmc-analysis/CHANGELOG.md",
|
|
24
|
-
"README.md",
|
|
25
|
-
"CHANGELOG.md"
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
const requiredFileGlobs = ["bin", "src", "scripts", "skill", "README.md", "CHANGELOG.md"];
|
|
29
|
-
const forbiddenPatterns = [
|
|
30
|
-
{ pattern: /@supabase\//, message: "CLI package must not import Supabase SDKs." },
|
|
31
|
-
{ pattern: /@clerk\//, message: "CLI package must not import Clerk SDKs." },
|
|
32
|
-
{ pattern: /src\/server|server\/gmc|server-only/, message: "CLI package must not import server-only app code." },
|
|
33
|
-
{ pattern: /SUPABASE_[A-Z0-9_]*|CLERK_[A-Z0-9_]*|STRIPE_[A-Z0-9_]*/, message: "CLI package must not reference server secret environment variables." }
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const sourceFiles = [
|
|
37
|
-
"bin/gmc.mjs",
|
|
38
|
-
"src/index.js",
|
|
39
|
-
"src/args.js",
|
|
40
|
-
"src/browser-login.js",
|
|
41
|
-
"src/config.js",
|
|
42
|
-
"src/constants.js",
|
|
43
|
-
"src/help.js",
|
|
44
|
-
"src/main.js",
|
|
45
|
-
"src/output.js",
|
|
46
|
-
"src/projection.js",
|
|
47
|
-
"src/requests.js",
|
|
48
|
-
"src/workflows.js"
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
const failures = [];
|
|
52
|
-
|
|
53
|
-
if (packageJson.private === true) {
|
|
54
|
-
failures.push("packages/gmc-cli/package.json must not have private:true for beta publishing.");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (packageJson.name !== "@witchpot/gmc") {
|
|
58
|
-
failures.push(`Unexpected package name: ${packageJson.name}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (packageJson.publishConfig?.access !== "public") {
|
|
62
|
-
failures.push("publishConfig.access must be public for npm beta-tag publishing.");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
for (const binName of ["gmc", "gmcp", "game-market-copilot"]) {
|
|
66
|
-
if (packageJson.bin?.[binName] !== "bin/gmc.mjs") {
|
|
67
|
-
failures.push(`bin.${binName} must point to bin/gmc.mjs.`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (packageJson.main !== "./src/index.js") {
|
|
72
|
-
failures.push("main must point to ./src/index.js.");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (packageJson.exports?.["."] !== "./src/index.js") {
|
|
76
|
-
failures.push("exports['.'] must point to ./src/index.js.");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
for (const required of requiredFileGlobs) {
|
|
80
|
-
if (!packageJson.files?.includes(required)) {
|
|
81
|
-
failures.push(`package.json files must include ${required}.`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
for (const file of requiredFiles) {
|
|
86
|
-
if (!existsSync(path.join(packageDir, file))) {
|
|
87
|
-
failures.push(`Missing required package file: ${file}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const constantsPath = path.join(packageDir, "src/constants.js");
|
|
92
|
-
const constantsSource = readFileSync(constantsPath, "utf8");
|
|
93
|
-
const versionMatch = constantsSource.match(/CLI_VERSION\s*=\s*"([^"]+)"/);
|
|
94
|
-
if (!versionMatch) {
|
|
95
|
-
failures.push("src/constants.js must export a string CLI_VERSION.");
|
|
96
|
-
} else if (versionMatch[1] !== packageJson.version) {
|
|
97
|
-
failures.push(`CLI_VERSION (${versionMatch[1]}) must match package version (${packageJson.version}).`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const changelog = readFileSync(path.join(packageDir, "CHANGELOG.md"), "utf8");
|
|
101
|
-
if (!changelog.includes(`## ${packageJson.version} -`)) {
|
|
102
|
-
failures.push(`CHANGELOG.md must include a section for ${packageJson.version}.`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const skillSource = readFileSync(path.join(packageDir, "skill/gmc-analysis/SKILL.md"), "utf8");
|
|
106
|
-
const skillVersionMatch = skillSource.match(/^\s*version:\s*"?([0-9][0-9A-Za-z.\-]*)"?\s*$/m);
|
|
107
|
-
if (!skillVersionMatch) {
|
|
108
|
-
failures.push("skill/gmc-analysis/SKILL.md must declare metadata.version in its frontmatter.");
|
|
109
|
-
} else {
|
|
110
|
-
const skillChangelog = readFileSync(path.join(packageDir, "skill/gmc-analysis/CHANGELOG.md"), "utf8");
|
|
111
|
-
if (!skillChangelog.includes(`## ${skillVersionMatch[1]} -`)) {
|
|
112
|
-
failures.push(`skill CHANGELOG.md must include a section for skill version ${skillVersionMatch[1]}.`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
for (const file of sourceFiles) {
|
|
117
|
-
const source = readFileSync(path.join(packageDir, file), "utf8");
|
|
118
|
-
for (const forbidden of forbiddenPatterns) {
|
|
119
|
-
if (forbidden.pattern.test(source)) {
|
|
120
|
-
failures.push(`${file}: ${forbidden.message}`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// The skill content-hash manifest (NAK-190) is also enforced by
|
|
126
|
-
// src/skill-manifest.test.js, but release:check/prepublishOnly run WITHOUT
|
|
127
|
-
// the test suite — recheck here so a stale manifest can never reach npm.
|
|
128
|
-
const { buildSkillManifest, MANIFEST_PATH } = await import("./gen-skill-manifest.mjs");
|
|
129
|
-
if (!existsSync(MANIFEST_PATH)) {
|
|
130
|
-
failures.push("skill/gmc-analysis.manifest.json is missing — run npm run skill:manifest.");
|
|
131
|
-
} else {
|
|
132
|
-
const committedManifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
133
|
-
const currentManifest = buildSkillManifest();
|
|
134
|
-
if (
|
|
135
|
-
committedManifest.contentHash !== currentManifest.contentHash ||
|
|
136
|
-
committedManifest.version !== currentManifest.version
|
|
137
|
-
) {
|
|
138
|
-
failures.push(
|
|
139
|
-
"skill/gmc-analysis.manifest.json is stale: bump metadata.version in SKILL.md, run npm run skill:manifest, and re-sync the gmc-agent-skills distribution repo."
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// The public distribution repo (github.com/witchpot-studio/gmc-agent-skills)
|
|
145
|
-
// mirrors skill/gmc-analysis. When it is checked out as a workspace sibling,
|
|
146
|
-
// a drift between the two is a release blocker — MCP-only users install the
|
|
147
|
-
// skill from that repo and must get the same content the npm package ships.
|
|
148
|
-
const explicitAgentSkillsDir = process.env.GMC_AGENT_SKILLS_DIR;
|
|
149
|
-
const agentSkillsRepo =
|
|
150
|
-
explicitAgentSkillsDir ?? path.join(packageDir, "..", "..", "..", "gmc-agent-skills");
|
|
151
|
-
const agentSkillsSyncScript = path.join(agentSkillsRepo, "scripts", "sync-from-cli.mjs");
|
|
152
|
-
if (explicitAgentSkillsDir && !existsSync(agentSkillsSyncScript)) {
|
|
153
|
-
failures.push(
|
|
154
|
-
`GMC_AGENT_SKILLS_DIR is set but ${agentSkillsSyncScript} does not exist — fix the path or unset the variable.`
|
|
155
|
-
);
|
|
156
|
-
} else if (existsSync(agentSkillsSyncScript)) {
|
|
157
|
-
const drift = spawnSync(process.execPath, ["scripts/sync-from-cli.mjs", "--check"], {
|
|
158
|
-
cwd: agentSkillsRepo,
|
|
159
|
-
encoding: "utf8",
|
|
160
|
-
// Pin the canonical source to THIS checkout — the sync script's own
|
|
161
|
-
// default (../GMC relative to the skills repo) could silently compare
|
|
162
|
-
// against a different GMC checkout.
|
|
163
|
-
env: { ...process.env, GMC_REPO_DIR: path.join(packageDir, "..", "..") }
|
|
164
|
-
});
|
|
165
|
-
if (drift.status !== 0) {
|
|
166
|
-
failures.push(
|
|
167
|
-
`gmc-agent-skills repo has drifted from skill/gmc-analysis — run node scripts/sync-from-cli.mjs there.\n${(drift.stderr || "").trim()}`
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (failures.length > 0) {
|
|
173
|
-
process.stderr.write(`GMC CLI release readiness failed:\n${failures.map((failure) => `- ${failure}`).join("\n")}\n`);
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
process.stdout.write(`GMC CLI release readiness ok: ${packageJson.name}@${packageJson.version}\n`);
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// Regenerates skill/gmc-analysis.manifest.json — a content-hash fingerprint
|
|
4
|
-
// of the packaged gmc-analysis skill (SKILL.md + references/ + CHANGELOG.md).
|
|
5
|
-
//
|
|
6
|
-
// Maintainer rule: any time skill/gmc-analysis/** content changes, bump
|
|
7
|
-
// metadata.version in SKILL.md, then run `npm run skill:manifest` (in
|
|
8
|
-
// packages/gmc-cli) to regenerate this manifest, and re-sync the
|
|
9
|
-
// gmc-agent-skills distribution repo (node scripts/sync-from-cli.mjs there).
|
|
10
|
-
// src/skill-manifest.test.js recomputes the same hash on every
|
|
11
|
-
// `npm run test:cli` / `npm run verify` and fails loudly if it no longer
|
|
12
|
-
// matches the committed manifest — this is the drift guard that would have
|
|
13
|
-
// caught the unversioned charts.md changes in PR #18 / PR #43 (NAK-190).
|
|
14
|
-
//
|
|
15
|
-
// The manifest is written OUTSIDE skill/gmc-analysis/ on purpose: that
|
|
16
|
-
// directory is mirrored byte-for-byte into the public gmc-agent-skills
|
|
17
|
-
// distribution repo, and the manifest must never be part of (or affect the
|
|
18
|
-
// hash of) the content it describes.
|
|
19
|
-
|
|
20
|
-
import { spawnSync } from "node:child_process";
|
|
21
|
-
import { createHash } from "node:crypto";
|
|
22
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
|
-
import path from "node:path";
|
|
24
|
-
import { fileURLToPath } from "node:url";
|
|
25
|
-
|
|
26
|
-
const packageDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
27
|
-
export const SKILL_DIR = path.join(packageDir, "skill", "gmc-analysis");
|
|
28
|
-
export const MANIFEST_PATH = path.join(packageDir, "skill", "gmc-analysis.manifest.json");
|
|
29
|
-
|
|
30
|
-
/** List every file under `dir`, as sorted relative POSIX paths. */
|
|
31
|
-
export function listSkillFiles(dir = SKILL_DIR) {
|
|
32
|
-
const results = [];
|
|
33
|
-
walk(dir, dir, results);
|
|
34
|
-
return results.sort();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function walk(root, current, results) {
|
|
38
|
-
const entries = readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
39
|
-
for (const entry of entries) {
|
|
40
|
-
const abs = path.join(current, entry.name);
|
|
41
|
-
if (entry.isDirectory()) {
|
|
42
|
-
walk(root, abs, results);
|
|
43
|
-
} else if (entry.isFile()) {
|
|
44
|
-
results.push(path.relative(root, abs).split(path.sep).join("/"));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function sha256Hex(buffer) {
|
|
50
|
-
return createHash("sha256").update(buffer).digest("hex");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Hash every file under `dir` and fold the per-file hashes into one
|
|
55
|
-
* top-level sha256 over `relpath\nfilehash\n` lines (sorted, deterministic).
|
|
56
|
-
*/
|
|
57
|
-
export function computeSkillContentHash(dir = SKILL_DIR) {
|
|
58
|
-
const files = listSkillFiles(dir);
|
|
59
|
-
const digest = createHash("sha256");
|
|
60
|
-
for (const relPath of files) {
|
|
61
|
-
const fileHash = sha256Hex(readFileSync(path.join(dir, relPath)));
|
|
62
|
-
digest.update(`${relPath}\n${fileHash}\n`);
|
|
63
|
-
}
|
|
64
|
-
return { contentHash: digest.digest("hex"), files: files.length };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Parse metadata.version out of SKILL.md source text; undefined when absent. */
|
|
68
|
-
export function parseSkillVersion(source) {
|
|
69
|
-
const frontmatter = source.match(/^---\n([\s\S]*?)\n---/);
|
|
70
|
-
return frontmatter?.[1].match(/^\s*version:\s*"?([0-9][0-9A-Za-z.\-]*)"?\s*$/m)?.[1];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Read metadata.version from SKILL.md's YAML frontmatter. */
|
|
74
|
-
export function readSkillManifestVersion(dir = SKILL_DIR) {
|
|
75
|
-
const version = parseSkillVersion(readFileSync(path.join(dir, "SKILL.md"), "utf8"));
|
|
76
|
-
if (!version) {
|
|
77
|
-
throw new Error(`${path.join(dir, "SKILL.md")} must declare metadata.version in its frontmatter.`);
|
|
78
|
-
}
|
|
79
|
-
return version;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Build the manifest object (version + contentHash + files) for `dir`. */
|
|
83
|
-
export function buildSkillManifest(dir = SKILL_DIR) {
|
|
84
|
-
const { contentHash, files } = computeSkillContentHash(dir);
|
|
85
|
-
return { version: readSkillManifestVersion(dir), contentHash, files };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Baseline manifest for the version-bump rule. The authoritative baseline is
|
|
90
|
-
* the manifest COMMITTED ON origin/main (read via `git show`, so deleting or
|
|
91
|
-
* hand-editing the local file cannot dodge the rule); the local file is only
|
|
92
|
-
* a fallback for environments where the git baseline is unreadable.
|
|
93
|
-
*/
|
|
94
|
-
function baselineSkillManifest() {
|
|
95
|
-
const result = spawnSync("git", ["show", "origin/main:packages/gmc-cli/skill/gmc-analysis.manifest.json"], {
|
|
96
|
-
cwd: packageDir,
|
|
97
|
-
encoding: "utf8"
|
|
98
|
-
});
|
|
99
|
-
if (result.status === 0 && typeof result.stdout === "string") {
|
|
100
|
-
try {
|
|
101
|
-
return JSON.parse(result.stdout);
|
|
102
|
-
} catch {
|
|
103
|
-
// fall through to the local file
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (existsSync(MANIFEST_PATH)) {
|
|
107
|
-
try {
|
|
108
|
-
return JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
109
|
-
} catch {
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Pure regen-permission rule (unit-tested in src/skill-manifest.test.js).
|
|
118
|
-
* Content that differs from the baseline while keeping the baseline's
|
|
119
|
-
* version is refused: once a version's content is on origin/main, changing
|
|
120
|
-
* content under that version always requires a bump. A differing version
|
|
121
|
-
* (the in-flight bump case, iterated freely within one branch) and a
|
|
122
|
-
* missing baseline (true first generation) are allowed. There is
|
|
123
|
-
* deliberately NO force escape hatch: an override flag would let
|
|
124
|
-
* unversioned content changes reach npm and the public skills repo.
|
|
125
|
-
*/
|
|
126
|
-
export function assertRegenAllowed({ baseline, next }) {
|
|
127
|
-
if (!baseline) return;
|
|
128
|
-
if (baseline.contentHash === next.contentHash) return;
|
|
129
|
-
if (baseline.version !== next.version) return;
|
|
130
|
-
throw new Error(
|
|
131
|
-
`skill content changed but metadata.version is still ${next.version}, whose content is already fixed by the baseline manifest (origin/main) — bump the version in SKILL.md, then rerun npm run skill:manifest.`
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function main() {
|
|
136
|
-
const manifest = buildSkillManifest();
|
|
137
|
-
try {
|
|
138
|
-
assertRegenAllowed({ baseline: baselineSkillManifest(), next: manifest });
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error(error.message);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
writeFileSync(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
144
|
-
process.stdout.write(
|
|
145
|
-
`Wrote ${path.relative(packageDir, MANIFEST_PATH)}: version ${manifest.version}, ${manifest.files} files, contentHash ${manifest.contentHash}\n`
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
150
|
-
main();
|
|
151
|
-
}
|
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
|
-
}
|