@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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the Game Market Copilot CLI will be recorded here.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - Unreleased
|
|
6
|
+
|
|
7
|
+
- Added the initial JSON-first `gmc` command set for status, schema, Steam game
|
|
8
|
+
search, resolve, detail, analysis, topics, compare, benchmark, review
|
|
9
|
+
patterns, Showcase search, and Showcase fit workflows.
|
|
10
|
+
- Added API-key login, logout, local config, environment-variable config, field
|
|
11
|
+
projection, and summary presets.
|
|
12
|
+
- Added a non-mutating `gmc update --json` command that reports the current
|
|
13
|
+
version and closed-beta npm update commands.
|
|
14
|
+
- Added the common evidence-pack shape for CLI-composed workflows.
|
|
15
|
+
- Marked the first closed-beta command set as supported and broader cohort
|
|
16
|
+
workflows as provisional.
|
|
17
|
+
- Prepared the package for public npm beta-tag publishing with metadata and
|
|
18
|
+
server-boundary release checks, packed-tarball smoke validation, and
|
|
19
|
+
`publish:beta` plus `publish:beta:dry-run` scripts.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Game Market Copilot CLI
|
|
2
|
+
|
|
3
|
+
`gmc` is the JSON-first command line client for Game Market Copilot. It wraps
|
|
4
|
+
stable `/api/v1` endpoints so Codex, Claude Code, and other agents can answer
|
|
5
|
+
market questions from bounded API evidence instead of reading Supabase directly.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
The CLI package is published to npm with a public package and beta dist-tag. If
|
|
10
|
+
the beta package has not been published in the current environment yet, run from
|
|
11
|
+
this repository:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm run gmc -- --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The package name for beta distribution is `@witchpot/gmc` with the
|
|
18
|
+
`gmc` binary. Before publishing a beta build, run:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run gmc:release:check
|
|
22
|
+
npm run gmc:publish:beta:dry-run
|
|
23
|
+
npm run gmc:publish:beta
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Closed-beta setup details live in
|
|
27
|
+
[`docs/closed-beta-onboarding.md`](../../docs/closed-beta-onboarding.md).
|
|
28
|
+
|
|
29
|
+
Closed-beta access is controlled by GMC API credentials, not npm package
|
|
30
|
+
visibility. Installing the public package does not grant access to paid or beta
|
|
31
|
+
data.
|
|
32
|
+
|
|
33
|
+
## Auth
|
|
34
|
+
|
|
35
|
+
Interactive browser login is the preferred local setup path:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gmc login
|
|
39
|
+
gmc auth status --json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use API-key auth for CI, headless agents, or restricted browser environments:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
gmc login --api-key <display-once workspace API key> --api-base-url https://gamemarketcopilot.com
|
|
46
|
+
gmc auth status --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Non-interactive agents and CI can use environment variables:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export GMC_API_BASE_URL=https://gamemarketcopilot.com
|
|
53
|
+
export GMC_API_KEY=<display-once workspace API key>
|
|
54
|
+
# or
|
|
55
|
+
export GMC_CLI_CREDENTIAL=<display-once browser-login CLI credential>
|
|
56
|
+
gmc auth status --json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The CLI never prints the raw key or credential after login. Status and config output show only
|
|
60
|
+
a non-secret prefix.
|
|
61
|
+
|
|
62
|
+
## First Queries
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
gmc schema --json
|
|
66
|
+
gmc update --json
|
|
67
|
+
gmc games resolve --source steam --query Timberborn --json
|
|
68
|
+
gmc games detail --source steam 1062090 --json
|
|
69
|
+
gmc campaigns signals --source steam 1062090 --json
|
|
70
|
+
gmc games benchmark --source steam --tags city_builder,automation --reviews-min 1000 --preset summary --json
|
|
71
|
+
gmc showcases fit --source steam 1062090 --json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Preserve `meta.requestId`, `meta.entitlements`, `warnings`, and `pagination`
|
|
75
|
+
when using output in agent answers.
|
|
76
|
+
|
|
77
|
+
## Supported Beta Commands
|
|
78
|
+
|
|
79
|
+
- `gmc status`
|
|
80
|
+
- `gmc auth status`
|
|
81
|
+
- `gmc schema`
|
|
82
|
+
- `gmc games search`
|
|
83
|
+
- `gmc games resolve`
|
|
84
|
+
- `gmc games detail`
|
|
85
|
+
- `gmc games analysis`
|
|
86
|
+
- `gmc campaigns signals`
|
|
87
|
+
- `gmc games topics`
|
|
88
|
+
- `gmc games compare`
|
|
89
|
+
- `gmc games benchmark`
|
|
90
|
+
- `gmc reviews patterns`
|
|
91
|
+
- `gmc showcases search`
|
|
92
|
+
- `gmc showcases fit`
|
|
93
|
+
|
|
94
|
+
Other cohort, marketing, creator, pricing, and showcase-pattern commands are
|
|
95
|
+
available as provisional evidence packs. Their names and output fields may still
|
|
96
|
+
change before the closed-beta contract is frozen.
|
|
97
|
+
|
|
98
|
+
## Updates
|
|
99
|
+
|
|
100
|
+
`gmc update --json` reports the current CLI version and the package-manager
|
|
101
|
+
commands to use after closed-beta npm distribution is available. It does not
|
|
102
|
+
mutate the local install yet.
|
|
103
|
+
|
|
104
|
+
## Beta Smoke
|
|
105
|
+
|
|
106
|
+
Run the beta smoke script with a production-like API and beta credential:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
GMC_API_BASE_URL=https://gamemarketcopilot.com \
|
|
110
|
+
GMC_API_KEY=<display-once workspace API key> \
|
|
111
|
+
npm run gmc:smoke:beta
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The script validates the JSON envelope for status, schema, title resolution,
|
|
115
|
+
detail, benchmark, and Showcase fit commands. It prints a redacted summary with
|
|
116
|
+
request IDs, warning counts, pagination presence, and entitlement metadata
|
|
117
|
+
presence. Non-JSON responses include HTTP status, content type, and request path
|
|
118
|
+
so Cloudflare Access interstitials can be diagnosed without printing HTML.
|
package/bin/gmc.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { isCliEntryPoint, runCliEntryPoint } from "../src/main.js";
|
|
4
|
+
|
|
5
|
+
export { BOOLEAN_OPTIONS, matches, parseArgs } from "../src/args.js";
|
|
6
|
+
export {
|
|
7
|
+
buildLoginUrl,
|
|
8
|
+
exchangeAuthorizationCode,
|
|
9
|
+
extractCredentialFromTokenPayload,
|
|
10
|
+
runBrowserLogin
|
|
11
|
+
} from "../src/browser-login.js";
|
|
12
|
+
export { handleLocalCommand, loadConfig, parseBaseUrl } from "../src/config.js";
|
|
13
|
+
export {
|
|
14
|
+
CLI_BETA_PACKAGE_NAME,
|
|
15
|
+
CLI_PACKAGE_NAME,
|
|
16
|
+
CLI_VERSION,
|
|
17
|
+
PROVISIONAL_BETA_COMMANDS,
|
|
18
|
+
SUPPORTED_BETA_COMMANDS
|
|
19
|
+
} from "../src/constants.js";
|
|
20
|
+
export { writeJsonError } from "../src/output.js";
|
|
21
|
+
export { applyFieldsProjection, applyOutputProjection } from "../src/projection.js";
|
|
22
|
+
export {
|
|
23
|
+
buildRequest,
|
|
24
|
+
buildRequestHeaders,
|
|
25
|
+
createRequestUrl,
|
|
26
|
+
filterShowcasesByDeadline
|
|
27
|
+
} from "../src/requests.js";
|
|
28
|
+
export {
|
|
29
|
+
buildHttpApiError,
|
|
30
|
+
buildNonJsonApiError,
|
|
31
|
+
composeBenchmarkPayload,
|
|
32
|
+
composeCampaignSignalsPayload,
|
|
33
|
+
composeCohortEvidencePayload,
|
|
34
|
+
composeComparePayload,
|
|
35
|
+
composeReviewPatternsPayload,
|
|
36
|
+
composeResolvePayload,
|
|
37
|
+
composeShowcaseFitPayload,
|
|
38
|
+
executeRequestPlan
|
|
39
|
+
} from "../src/workflows.js";
|
|
40
|
+
export {
|
|
41
|
+
buildCliAuthStatus,
|
|
42
|
+
enrichAuthStatusOutput,
|
|
43
|
+
isCliEntryPoint,
|
|
44
|
+
main,
|
|
45
|
+
runCliEntryPoint
|
|
46
|
+
} from "../src/main.js";
|
|
47
|
+
|
|
48
|
+
if (isCliEntryPoint(import.meta.url)) {
|
|
49
|
+
runCliEntryPoint();
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@witchpot/gmc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "JSON-first CLI for Game Market Copilot API and agent workflows.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/witchpot/game-market-copilot.git",
|
|
10
|
+
"directory": "packages/gmc-cli"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://gamemarketcopilot.com",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://gamemarketcopilot.com"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"bin": {
|
|
18
|
+
"gmc": "bin/gmc.mjs"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20.9.0"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"src",
|
|
26
|
+
"scripts",
|
|
27
|
+
"README.md",
|
|
28
|
+
"CHANGELOG.md"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"release:check": "node scripts/check-release-readiness.mjs && node scripts/smoke-pack.mjs",
|
|
35
|
+
"prepublishOnly": "npm run release:check",
|
|
36
|
+
"smoke:beta": "node scripts/smoke-beta.mjs",
|
|
37
|
+
"pack:check": "node scripts/smoke-pack.mjs --dry-run",
|
|
38
|
+
"smoke:pack": "node scripts/smoke-pack.mjs",
|
|
39
|
+
"publish:beta:dry-run": "npm publish --tag beta --access public --dry-run --cache /tmp/gmc-npm-cache",
|
|
40
|
+
"publish:beta": "npm publish --tag beta --access public --cache /tmp/gmc-npm-cache"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const packageDir = path.resolve(new URL("..", import.meta.url).pathname);
|
|
7
|
+
const packageJsonPath = path.join(packageDir, "package.json");
|
|
8
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
9
|
+
|
|
10
|
+
const requiredFiles = [
|
|
11
|
+
"bin/gmc.mjs",
|
|
12
|
+
"src/main.js",
|
|
13
|
+
"src/requests.js",
|
|
14
|
+
"src/workflows.js",
|
|
15
|
+
"README.md",
|
|
16
|
+
"CHANGELOG.md"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const requiredFileGlobs = ["bin", "src", "scripts", "README.md", "CHANGELOG.md"];
|
|
20
|
+
const forbiddenPatterns = [
|
|
21
|
+
{ pattern: /@supabase\//, message: "CLI package must not import Supabase SDKs." },
|
|
22
|
+
{ pattern: /@clerk\//, message: "CLI package must not import Clerk SDKs." },
|
|
23
|
+
{ pattern: /src\/server|server\/gmc|server-only/, message: "CLI package must not import server-only app code." },
|
|
24
|
+
{ pattern: /SUPABASE_[A-Z0-9_]*|CLERK_[A-Z0-9_]*|STRIPE_[A-Z0-9_]*/, message: "CLI package must not reference server secret environment variables." }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const sourceFiles = [
|
|
28
|
+
"bin/gmc.mjs",
|
|
29
|
+
"src/args.js",
|
|
30
|
+
"src/browser-login.js",
|
|
31
|
+
"src/config.js",
|
|
32
|
+
"src/constants.js",
|
|
33
|
+
"src/help.js",
|
|
34
|
+
"src/main.js",
|
|
35
|
+
"src/output.js",
|
|
36
|
+
"src/projection.js",
|
|
37
|
+
"src/requests.js",
|
|
38
|
+
"src/workflows.js"
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const failures = [];
|
|
42
|
+
|
|
43
|
+
if (packageJson.private === true) {
|
|
44
|
+
failures.push("packages/gmc-cli/package.json must not have private:true for beta publishing.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (packageJson.name !== "@witchpot/gmc") {
|
|
48
|
+
failures.push(`Unexpected package name: ${packageJson.name}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (packageJson.publishConfig?.access !== "public") {
|
|
52
|
+
failures.push("publishConfig.access must be public for npm beta-tag publishing.");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (packageJson.bin?.gmc !== "bin/gmc.mjs") {
|
|
56
|
+
failures.push("bin.gmc must point to bin/gmc.mjs.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const required of requiredFileGlobs) {
|
|
60
|
+
if (!packageJson.files?.includes(required)) {
|
|
61
|
+
failures.push(`package.json files must include ${required}.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const file of requiredFiles) {
|
|
66
|
+
if (!existsSync(path.join(packageDir, file))) {
|
|
67
|
+
failures.push(`Missing required package file: ${file}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const constantsPath = path.join(packageDir, "src/constants.js");
|
|
72
|
+
const constantsSource = readFileSync(constantsPath, "utf8");
|
|
73
|
+
const versionMatch = constantsSource.match(/CLI_VERSION\s*=\s*"([^"]+)"/);
|
|
74
|
+
if (!versionMatch) {
|
|
75
|
+
failures.push("src/constants.js must export a string CLI_VERSION.");
|
|
76
|
+
} else if (versionMatch[1] !== packageJson.version) {
|
|
77
|
+
failures.push(`CLI_VERSION (${versionMatch[1]}) must match package version (${packageJson.version}).`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const changelog = readFileSync(path.join(packageDir, "CHANGELOG.md"), "utf8");
|
|
81
|
+
if (!changelog.includes(`## ${packageJson.version} -`)) {
|
|
82
|
+
failures.push(`CHANGELOG.md must include a section for ${packageJson.version}.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const file of sourceFiles) {
|
|
86
|
+
const source = readFileSync(path.join(packageDir, file), "utf8");
|
|
87
|
+
for (const forbidden of forbiddenPatterns) {
|
|
88
|
+
if (forbidden.pattern.test(source)) {
|
|
89
|
+
failures.push(`${file}: ${forbidden.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (failures.length > 0) {
|
|
95
|
+
process.stderr.write(`GMC CLI release readiness failed:\n${failures.map((failure) => `- ${failure}`).join("\n")}\n`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
process.stdout.write(`GMC CLI release readiness ok: ${packageJson.name}@${packageJson.version}\n`);
|
|
@@ -0,0 +1,220 @@
|
|
|
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: "resolve Timberborn",
|
|
37
|
+
args: ["games", "resolve", "--source", "steam", "--query", "Timberborn", "--json"],
|
|
38
|
+
validate: (payload) => requireComposedEnvelope(payload, ["data.candidates", "data.resolution"])
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "detail Timberborn",
|
|
42
|
+
args: ["games", "detail", "--source", "steam", "1062090", "--json"],
|
|
43
|
+
validate: (payload) => requireSuccessEnvelope(payload, ["data.title"])
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "benchmark city builder automation",
|
|
47
|
+
args: [
|
|
48
|
+
"games",
|
|
49
|
+
"benchmark",
|
|
50
|
+
"--source",
|
|
51
|
+
"steam",
|
|
52
|
+
"--tags",
|
|
53
|
+
"city_builder,automation",
|
|
54
|
+
"--reviews-min",
|
|
55
|
+
"1000",
|
|
56
|
+
"--preset",
|
|
57
|
+
"summary",
|
|
58
|
+
"--json"
|
|
59
|
+
],
|
|
60
|
+
validate: (payload) => requireComposedEnvelope(payload, ["data.summary"])
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "showcase fit Timberborn",
|
|
64
|
+
args: ["showcases", "fit", "--source", "steam", "1062090", "--json"],
|
|
65
|
+
validate: (payload) => requireComposedEnvelope(payload, ["data.matches", "data.summary"])
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const results = smokeCommands.map(runSmokeCommand);
|
|
71
|
+
const failed = results.filter((result) => result.status === "failed");
|
|
72
|
+
const summary = {
|
|
73
|
+
ok: failed.length === 0,
|
|
74
|
+
apiBaseUrl: process.env.GMC_API_BASE_URL || "https://gamemarketcopilot.com",
|
|
75
|
+
auth: {
|
|
76
|
+
apiKeyConfigured: Boolean(process.env.GMC_API_KEY)
|
|
77
|
+
},
|
|
78
|
+
allowApiErrors,
|
|
79
|
+
commands: results
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
83
|
+
if (failed.length > 0) process.exit(1);
|
|
84
|
+
} finally {
|
|
85
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runSmokeCommand(command) {
|
|
89
|
+
const result = spawnSync(process.execPath, [cliBin, ...command.args], {
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
env: {
|
|
92
|
+
...process.env,
|
|
93
|
+
GMC_CONFIG_PATH: configPath
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const payload = parseJson(result.stdout);
|
|
98
|
+
if (!payload.ok) {
|
|
99
|
+
return {
|
|
100
|
+
name: command.name,
|
|
101
|
+
status: "failed",
|
|
102
|
+
exitCode: result.status,
|
|
103
|
+
error: `stdout was not valid JSON: ${payload.error}`
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (payload.value?.error) {
|
|
108
|
+
const stable = isStableError(payload.value.error);
|
|
109
|
+
const status = allowApiErrors && stable ? "api_error_allowed" : "failed";
|
|
110
|
+
return {
|
|
111
|
+
name: command.name,
|
|
112
|
+
status,
|
|
113
|
+
exitCode: result.status,
|
|
114
|
+
error: summarizeError(payload.value.error)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (result.status !== 0) {
|
|
119
|
+
return {
|
|
120
|
+
name: command.name,
|
|
121
|
+
status: "failed",
|
|
122
|
+
exitCode: result.status,
|
|
123
|
+
error: "command exited non-zero without a structured error envelope"
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const validation = command.validate(payload.value);
|
|
128
|
+
if (!validation.ok) {
|
|
129
|
+
return {
|
|
130
|
+
name: command.name,
|
|
131
|
+
status: "failed",
|
|
132
|
+
exitCode: result.status,
|
|
133
|
+
error: validation.error
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
name: command.name,
|
|
139
|
+
status: "passed",
|
|
140
|
+
exitCode: result.status,
|
|
141
|
+
requestId: payload.value.meta?.requestId,
|
|
142
|
+
upstreamRequestIds: Array.isArray(payload.value.meta?.upstreamRequests)
|
|
143
|
+
? payload.value.meta.upstreamRequests.map((request) => request?.requestId).filter(Boolean)
|
|
144
|
+
: undefined,
|
|
145
|
+
warnings: Array.isArray(payload.value.warnings) ? payload.value.warnings.length : 0,
|
|
146
|
+
hasPagination: Boolean(payload.value.pagination),
|
|
147
|
+
hasEntitlements: Boolean(payload.value.meta?.entitlements),
|
|
148
|
+
hasUpstreamEntitlements: Array.isArray(payload.value.meta?.upstreamRequests)
|
|
149
|
+
? payload.value.meta.upstreamRequests.some((request) => request?.entitlements)
|
|
150
|
+
: undefined
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseJson(stdout) {
|
|
155
|
+
try {
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
value: JSON.parse(stdout)
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
error: error instanceof Error ? error.message : "unknown parse error"
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function requireSuccessEnvelope(payload, fields) {
|
|
169
|
+
const base = requireFields(payload, ["data", "meta.requestId", ...fields]);
|
|
170
|
+
if (!base.ok) return base;
|
|
171
|
+
return {
|
|
172
|
+
ok: true
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function requireComposedEnvelope(payload, fields) {
|
|
177
|
+
const base = requireFields(payload, ["data", "meta.requestId", "meta.upstreamRequests", ...fields]);
|
|
178
|
+
if (!base.ok) return base;
|
|
179
|
+
if (!Array.isArray(payload.meta.upstreamRequests) || payload.meta.upstreamRequests.length === 0) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
error: "meta.upstreamRequests must include at least one request"
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
ok: true
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function requireFields(payload, fields) {
|
|
191
|
+
const missing = fields.filter((field) => getPath(payload, field) == null);
|
|
192
|
+
if (missing.length > 0) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
error: `missing fields: ${missing.join(", ")}`
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
ok: true
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getPath(value, pathName) {
|
|
204
|
+
return pathName.split(".").reduce((current, part) => current?.[part], value);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isStableError(error) {
|
|
208
|
+
return Boolean(error && typeof error.code === "string" && typeof error.message === "string");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function summarizeError(error) {
|
|
212
|
+
return {
|
|
213
|
+
code: error.code,
|
|
214
|
+
message: error.message,
|
|
215
|
+
requestId: error.requestId,
|
|
216
|
+
status: error.status,
|
|
217
|
+
contentType: error.contentType,
|
|
218
|
+
path: error.path
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
run(gmcBin, ["--help"], { env: { GMC_CONFIG_PATH: configPath } });
|
|
52
|
+
run(gmcBin, ["config", "get", "--json"], { env: { GMC_CONFIG_PATH: configPath } });
|
|
53
|
+
} finally {
|
|
54
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function run(command, args, options = {}) {
|
|
58
|
+
const result = spawnSync(command, args, {
|
|
59
|
+
cwd: options.cwd,
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
env: {
|
|
62
|
+
...process.env,
|
|
63
|
+
npm_config_dry_run: "false",
|
|
64
|
+
...(options.env ?? {})
|
|
65
|
+
},
|
|
66
|
+
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit"
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (result.status !== 0) {
|
|
70
|
+
const stderr = result.stderr ? `\n${result.stderr}` : "";
|
|
71
|
+
throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status}${stderr}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}
|