@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 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
+ }