@qaping/cli 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/bin/qaping ADDED
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ // qaping — automatic QA for game developers, as a thin brand wrapper over the
3
+ // pingfusi kit (a real dependency; same accounts, credits, and service under
4
+ // the hood). Every command resolves the installed kit and drives it with the
5
+ // qaping wrapper options — MCP mount /api/mcp/qaping, MCP server key "qaping",
6
+ // the qaping skill + always-loaded rule — so the customer only ever sees
7
+ // qaping, and the kit's own pingfusi instruction surfaces are neither
8
+ // installed nor swept (the vendored installer's --skip-instruction-surfaces
9
+ // contract).
10
+ //
11
+ // qaping setup [claude-code|cursor|codex] [--force] one-command onboarding
12
+ // qaping remove|uninstall [--client <c>] sweep ONLY qaping's state
13
+ // qaping publish-build <game.zip> --platform windows|macos
14
+ // qaping wait <ping_id> [--timeout <seconds>]
15
+ // qaping whoami | version | help
16
+ "use strict";
17
+ const fs = require("fs");
18
+ const os = require("os");
19
+ const path = require("path");
20
+ const readline = require("readline");
21
+ const { spawnSync } = require("child_process");
22
+
23
+ const QPKG = path.resolve(__dirname, "..");
24
+
25
+ // The kit is a normal npm dependency when qaping is installed, and a workspace
26
+ // sibling in the monorepo checkout (where node_modules may not be linked yet)
27
+ // — resolve the package, never a hardcoded install path.
28
+ function resolveKitDir() {
29
+ try {
30
+ return path.dirname(require.resolve("pingfusi/package.json", { paths: [QPKG] }));
31
+ } catch {
32
+ const sibling = path.join(QPKG, "..", "kit");
33
+ try {
34
+ if (require(path.join(sibling, "package.json")).name === "pingfusi") return sibling;
35
+ } catch { /* fall through */ }
36
+ throw new Error("cannot resolve the pingfusi kit (a dependency of qaping) — reinstall: npm i -g qaping");
37
+ }
38
+ }
39
+
40
+ // The always-loaded rule (the qaping counterpart of the installer's RULE_BODY):
41
+ // short, WHEN to reach for qaping; the skill owns the full loop. agent-setup
42
+ // writes it to ~/.claude/rules/qaping.md and ~/.cursor/rules/qaping.mdc.
43
+ const RULE_BODY = `This machine has qaping: automatic QA for games — real human playtesters play the developer's builds on their own hardware and report back, and the coding agent runs the whole loop.
44
+ Whenever the user asks to set up QA for a game, run QA on a patch, check whether a change broke anything, or playtest a game or build, load the qaping skill and follow it — it owns authoring/maintaining QA-PLAN.md in the game repo and the per-patch run.
45
+ Builds ship with \`qaping publish-build <game.zip> --platform windows|macos\`; the printed /b/<slug> URL is what a round is filed against (store-delivered games file with their Steam/TestFlight/App Store URL instead).
46
+ The MCP tools are qaping_playtest (file a round with real human playtesters), qaping_results (fetch a finished round, free) and qaping_wait (continue a pending round — whenever a filing or wait returns pending, call qaping_wait again immediately; never report pending as the answer).
47
+ Playtests are duration-billed at 2 credits per minute of play per playtester — state the estimated cost before filing. Windows rounds return a recording + questionnaire and no transcript; never promise one.
48
+ `;
49
+
50
+ // The wrapper object harness/setup.js's opts.wrapper API takes; also the one
51
+ // source for the vendored installer's brand flags (spawned per command below).
52
+ function qapingWrapper() {
53
+ return {
54
+ brand: "qaping",
55
+ // npm registry name ≠ bin name: the similarity rule blocks bare "qaping"
56
+ // (petition open, QAPING_PLAN §8), so the global install pulls @qaping/cli.
57
+ installPackage: "@qaping/cli",
58
+ appUrl: process.env.QAPING_APP_URL || undefined,
59
+ mcpPath: "/api/mcp/qaping",
60
+ serverKey: "qaping",
61
+ skipInstructionSurfaces: true,
62
+ skillRoot: path.join(QPKG, "skill"),
63
+ ruleAsset: { fileBaseName: "qaping", body: RULE_BODY },
64
+ };
65
+ }
66
+
67
+ function vendorFlags() {
68
+ const w = qapingWrapper();
69
+ return [
70
+ ...(w.appUrl ? ["--app-url", w.appUrl] : []),
71
+ "--mcp-path", w.mcpPath,
72
+ "--server-key", w.serverKey,
73
+ "--skip-instruction-surfaces",
74
+ ];
75
+ }
76
+
77
+ // One vendored-installer invocation per command, brand flags AFTER the
78
+ // command/user args (the installer parses flags positionally-agnostic, but
79
+ // `wait` takes the first non-flag arg as the ping id — user args stay first).
80
+ function spawnVendor(args) {
81
+ const vendor = path.join(resolveKitDir(), "vendor", "pingfusi-review.mjs");
82
+ const r = spawnSync(process.execPath, [vendor, ...args, ...vendorFlags()], { stdio: "inherit" });
83
+ return r.status == null ? 1 : r.status;
84
+ }
85
+
86
+ // Running from the monorepo checkout (not an npm install): the repo root's
87
+ // .git two levels up, and this file not under any node_modules. The plain
88
+ // "..git two up" test alone would also match node_modules/qaping inside a
89
+ // user's git project — that is an install, not a checkout.
90
+ function isSourceCheckout() {
91
+ return !/node_modules/.test(QPKG) && fs.existsSync(path.join(QPKG, "..", "..", ".git"));
92
+ }
93
+
94
+ // Same io shape harness/setup.js's defaultIO builds (it isn't exported; the
95
+ // kit-only steps a wrapper brand skips mean this needs no probe machinery
96
+ // beyond the contract). npm/which route through the kit's proc.js — it owns
97
+ // the Windows npm.cmd/where split.
98
+ function defaultIO(kitDir) {
99
+ const { spawnNpmSync, whichSync } = require(path.join(kitDir, "harness", "proc.js"));
100
+ return {
101
+ isTTY: !!process.stdin.isTTY,
102
+ log: (...a) => console.log(...a),
103
+ run: (cmd, args) => (cmd === "npm"
104
+ ? spawnNpmSync(args, { stdio: "inherit" })
105
+ : spawnSync(cmd, args, { stdio: "inherit" })),
106
+ probe: (cmd, args) => {
107
+ try {
108
+ const r = spawnSync(cmd, args, { stdio: "pipe", timeout: 10_000 });
109
+ return !r.error && (r.status === 0 || !!((r.stdout && r.stdout.length) || (r.stderr && r.stderr.length)));
110
+ } catch { return false; }
111
+ },
112
+ which: (cmd) => whichSync(cmd),
113
+ ask: (q) =>
114
+ new Promise((res) => {
115
+ if (!process.stdin.isTTY) return res("");
116
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
117
+ rl.question(q, (a) => { rl.close(); res(a.trim().toLowerCase()); });
118
+ }),
119
+ };
120
+ }
121
+
122
+ const VALID_CLIENTS = ["claude-desktop", "claude-code", "cursor", "codex"];
123
+
124
+ async function cmdSetup(argv) {
125
+ const kitDir = resolveKitDir();
126
+ const { setup } = require(path.join(kitDir, "harness", "setup.js"));
127
+ const { resolveToken } = require(path.join(kitDir, "harness", "review-qa.js"));
128
+ // accepts both the positional form (`setup cursor`) and `setup --client cursor`
129
+ const args = argv.filter((a) => a !== "--force");
130
+ const client = ((args[1] === "--client" ? args[2] : args[1]) || "").toLowerCase();
131
+ const r = await setup(defaultIO(kitDir), {
132
+ home: os.homedir(),
133
+ sourceCheckout: isSourceCheckout(),
134
+ resolveToken,
135
+ force: argv.includes("--force"),
136
+ dittoApiKey: false,
137
+ mcpClient: VALID_CLIENTS.includes(client) ? client : null,
138
+ wrapper: qapingWrapper(),
139
+ });
140
+ if (r.ok) {
141
+ console.log(`
142
+ Ask your agent:
143
+ "Set up QA for my game." (authors QA-PLAN.md in your repo)
144
+ "Run QA on this patch." (builds, ships, files playtest rounds)
145
+ "Did this patch break anything?" (the same loop, asked plainly)
146
+
147
+ Playtest rounds are answered by real human playtesters on their own hardware;
148
+ your agent files them, reads the results, and reports back.
149
+ (re-run anytime: qaping setup)`);
150
+ }
151
+ process.exit(r.ok ? 0 : 1);
152
+ }
153
+
154
+ function cmdRemove(argv) {
155
+ const kitDir = resolveKitDir();
156
+ // The kit bin's client-scope parser: --client validation is a hard error
157
+ // BEFORE anything destructive runs, never a scope widening.
158
+ const { kitSkillClient } = require(path.join(kitDir, "bin", "pingfusi"));
159
+ let client;
160
+ try { client = kitSkillClient(argv); }
161
+ catch (e) { console.error(`✗ ${e.message}`); process.exit(2); }
162
+ if (client !== "claude-desktop") { // desktop has MCP config but no coding-agent skills
163
+ try {
164
+ const w = qapingWrapper();
165
+ const removed = require(path.join(kitDir, "harness", "agent-setup.js"))
166
+ .removeSkills(os.homedir(), client, { skillRoot: w.skillRoot, ruleAsset: w.ruleAsset });
167
+ if (removed.length) console.log(`✓ Removed qaping agent skill(s): ${removed.join(", ")}`);
168
+ } catch { /* best-effort — the installer's own removal still runs */ }
169
+ }
170
+ // The vendored remove with qaping's flags sweeps ONLY the qaping MCP entries
171
+ // and never touches the shared ~/.config/pingfusi login or pingfusi's own
172
+ // rule/skill files (--skip-instruction-surfaces + a non-pingfusi server key).
173
+ process.exit(spawnVendor(argv));
174
+ }
175
+
176
+ function cmdPublishBuild(argv) {
177
+ // Kit passthrough: harness/publish-build.js main() takes argv AFTER the
178
+ // command (argv[0] = the zip path) and owns its own exit codes. The brand
179
+ // seam keeps every printed command/tool name qaping's own: usage says
180
+ // `qaping publish-build`, and the next step names qaping_playtest — the
181
+ // tool the /api/mcp/qaping mount actually registers.
182
+ return require(path.join(resolveKitDir(), "harness", "publish-build.js"))
183
+ .main(argv.slice(1), { brandCommand: "qaping publish-build", nextStepToolName: "qaping_playtest" });
184
+ }
185
+
186
+ const HELP = `qaping — automatic QA for your game: real human playtesters, driven by your coding agent
187
+
188
+ usage:
189
+ qaping setup [claude-code|cursor|codex] [--force]
190
+ qaping remove [--client <c>] remove qaping's MCP entries, skill and rule
191
+ qaping publish-build <game.zip> --platform windows|macos
192
+ qaping wait <ping_id> [--timeout <seconds>]
193
+ qaping whoami
194
+ qaping version
195
+
196
+ Then ask your agent: "Set up QA for my game." or "Run QA on this patch."`;
197
+
198
+ function route(cmd) {
199
+ if (cmd === "version" || cmd === "--version" || cmd === "-v") return "version";
200
+ if (cmd === "help" || cmd === "--help" || cmd === "-h") return "help";
201
+ if (cmd === "setup") return "setup";
202
+ if (cmd === "remove" || cmd === "uninstall") return "remove";
203
+ if (cmd === "wait" || cmd === "whoami") return "vendor";
204
+ if (cmd === "publish-build") return "publish-build";
205
+ return "unknown";
206
+ }
207
+
208
+ function main() {
209
+ const argv = process.argv.slice(2);
210
+ try {
211
+ switch (route(argv[0])) {
212
+ case "version": return console.log(require("../package.json").version);
213
+ case "help": return console.log(HELP);
214
+ case "setup": return void cmdSetup(argv).catch((e) => { console.error(`✗ ${(e && e.message) || e}`); process.exit(1); });
215
+ case "remove": return cmdRemove(argv);
216
+ // `wait` also names the wait tool the qaping mount registers — the
217
+ // vendored default (the stock mount's wait) is not on /api/mcp/qaping.
218
+ case "vendor": return process.exit(spawnVendor(argv[0] === "wait" ? [...argv, "--wait-tool", "qaping_wait"] : argv));
219
+ case "publish-build": return void cmdPublishBuild(argv);
220
+ default:
221
+ console.error(HELP);
222
+ process.exit(1);
223
+ }
224
+ } catch (e) {
225
+ // a clean message for sync failures (e.g. the kit dependency missing) —
226
+ // never a raw stack at the user
227
+ console.error(`✗ ${(e && e.message) || e}`);
228
+ process.exit(1);
229
+ }
230
+ }
231
+
232
+ if (require.main === module) main();
233
+ module.exports = { route, resolveKitDir, qapingWrapper, vendorFlags, isSourceCheckout, RULE_BODY, HELP };
@@ -0,0 +1,71 @@
1
+ # QA-PLAN.md — the format
2
+
3
+ `QA-PLAN.md` lives at the GAME repo's root. The agent authors and maintains it;
4
+ the developer owns it and approves every commit. Agents parse it by reading —
5
+ keep it exactly this shape, nothing fancier.
6
+
7
+ ## Plan frontmatter
8
+
9
+ YAML at the very top of the file:
10
+
11
+ ```yaml
12
+ ---
13
+ game: Solar Drift
14
+ build_command: ./scripts/package.sh --release # the ONE command that produces a shippable build
15
+ platforms: [windows] # windows | macos | ios
16
+ input: XInput gamepad # optional; default keyboard+mouse
17
+ ---
18
+ ```
19
+
20
+ `build_command` is recorded once at setup so every future run can build without
21
+ asking. `input` declares the hardware the first-15-minutes check (and any check
22
+ that needs it) is played on — rounds must ask playtesters to confirm what they
23
+ actually used.
24
+
25
+ ## Checks
26
+
27
+ One `##` section per check; the heading is the check's short name. The section
28
+ opens with a fenced yaml block, then the steps as prose:
29
+
30
+ ```yaml
31
+ id: save-loads # stable slug — never reused, never renamed
32
+ rung: human # human | code
33
+ origin: authored # "authored", or the round id that minted/last changed it
34
+ last_verified: 2026-08-25 # date or build tag of the last passing verification
35
+ ```
36
+
37
+ - **Steps are prose for a person**: numbered, imperative, written so a
38
+ playtester who has never seen the game can follow them — they become the
39
+ round's instructions verbatim.
40
+ - **Order the file critical-path first**: boots to menu, previous-version save
41
+ loads, first 15 minutes on the declared input hardware, core loop, settings
42
+ persist — then game-specific checks.
43
+ - A `rung: code` check keeps its prose (it documents intent) and adds one
44
+ `test:` line naming the repo test that now asserts it, e.g.
45
+ `` test: `tests/save_compat.test.ts` ``.
46
+
47
+ ## Example section
48
+
49
+ ````markdown
50
+ ## A previous-version save loads
51
+
52
+ ```yaml
53
+ id: save-loads
54
+ rung: human
55
+ origin: authored
56
+ last_verified: 2026-08-25
57
+ ```
58
+
59
+ 1. From the main menu, choose Continue.
60
+ 2. Load the provided save "campaign-mid.sav" (made on the previous release).
61
+ 3. Confirm the game resumes in the desert outpost with the inventory intact.
62
+ 4. Play for one minute; note anything missing, corrupted, or visually wrong.
63
+ ````
64
+
65
+ ## Maintenance rules
66
+
67
+ - Every run updates the run checks' `last_verified`; findings-driven edits
68
+ record the round id as `origin`.
69
+ - New checks start `rung: human`, `origin: authored`.
70
+ - Promotion (human → code) and any deletion/demotion happen only with the
71
+ developer's explicit agreement.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@qaping/cli",
3
+ "version": "0.1.0",
4
+ "description": "qaping: automatic QA for your game. Your coding agent authors and maintains the QA plan, ships each patch's build to real human playtesters, and reports what broke.",
5
+ "keywords": [
6
+ "qaping",
7
+ "game-qa",
8
+ "playtest",
9
+ "regression",
10
+ "qa",
11
+ "agent"
12
+ ],
13
+ "license": "MIT",
14
+ "type": "commonjs",
15
+ "bin": {
16
+ "qaping": "bin/qaping"
17
+ },
18
+ "files": [
19
+ "bin/",
20
+ "skill/",
21
+ "docs/"
22
+ ],
23
+ "engines": {
24
+ "node": "^20.17.0 || ^22.13.0 || >=23.5.0"
25
+ },
26
+ "dependencies": {
27
+ "pingfusi": "^0.16.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: qaping
3
+ description: Run automatic QA for a game repo through qaping — real human playtesters play the developer's builds on their own hardware, and the agent runs the whole loop. Use when the user says "set up QA for my game", "run QA on this patch", "did this patch break anything", "playtest my game" or "playtest this build", or asks for regression checks, patch verification, or human playtesting of a game. Covers both halves of the product - authoring and maintaining QA-PLAN.md in the game repo, and the per-patch run (read the diff, build, publish-build, file qaping_playtest rounds, report on the PR, file issues, promote settled checks into repo code tests). Do not use for web apps, websites, or UI review of anything that is not a game build — the qaping tools test game builds only.
4
+ ---
5
+
6
+ # qaping — the QA loop for a game repo
7
+
8
+ Two jobs, one file. SETUP authors `QA-PLAN.md` — the committed list of checks
9
+ this game must pass. RUN executes it per patch: pick the affected checks, build,
10
+ ship the build to real human playtesters, report what they found. Over patches,
11
+ checks migrate from human rounds to repo code tests, so QA gets cheaper. All
12
+ service contact goes through three MCP tools — `qaping_playtest`,
13
+ `qaping_results`, `qaping_wait` — plus `qaping publish-build` for hosting
14
+ the build.
15
+
16
+ ## Ground rules (read before filing anything)
17
+
18
+ - **Playtesters are real people.** A session takes real minutes to be claimed
19
+ and played. File, then `qaping_wait`; whenever a filing or wait returns
20
+ pending, immediately call `qaping_wait` again with the same ping_id. Never
21
+ report pending as the final answer, never file a duplicate round.
22
+ - **Costs are duration-billed**: 2 credits per minute of play, per playtester
23
+ (`est_minutes × 2 × players`). State the estimate to the user BEFORE filing.
24
+ - **Windows rounds return a recording + questionnaire and NO transcript**
25
+ (`transcript_status:'unavailable'`) — never promise one; plan to watch the
26
+ recording for timestamps. macOS and iOS rounds include an inline [mm:ss]
27
+ think-aloud transcript.
28
+ - **Machine replay is not self-serve yet.** Never claim an automated replay ran.
29
+ The only automated rung you can run today is a `rung: code` test in the
30
+ dev's own repo/CI.
31
+ - **QA-PLAN.md is the dev's file.** Commit it only with their approval; never
32
+ delete or demote a human check without them agreeing.
33
+
34
+ ## SETUP — author QA-PLAN.md (first run in a repo)
35
+
36
+ 1. Read the game repo: engine, how a shippable build is produced, save system,
37
+ settings, input devices the game supports.
38
+ 2. Ask the dev ONCE for their build command (the one command that produces a
39
+ shippable build) and which platforms they ship; record both in the plan's
40
+ frontmatter so every future run can build without asking again.
41
+ 3. Author `QA-PLAN.md` at the repo root, format per `docs/QA-PLAN-FORMAT.md`
42
+ (shipped with this package — read it first). Order checks critical-path
43
+ first, then game-specific ones:
44
+ 1. the game boots to the main menu
45
+ 2. a previous-version save loads
46
+ 3. the first 15 minutes play clean on the declared input hardware
47
+ 4. the core loop works (name it concretely — the thing this game is)
48
+ 5. settings persist across a restart
49
+ Every check carries `{id, rung, origin, last_verified}`. New checks start
50
+ `rung: human`, `origin: authored`.
51
+ 4. Tell the dev how runs get triggered, honestly: for now the loop runs from
52
+ their own machine or agent — after a patch lands, they say "run QA on this
53
+ patch" and the RUN half below executes. Do NOT offer to wire a CI workflow
54
+ file: a plain CI runner has no qaping MCP connection and no login, so a
55
+ committed workflow could not run this loop today. A CI-triggered recipe is
56
+ a documented follow-up; until it ships, per-patch QA is an ask-your-agent
57
+ step.
58
+ 5. Show the dev the plan and commit only on their approval.
59
+
60
+ ## RUN — per patch
61
+
62
+ 1. **Select.** Read the diff (or PR). Pick the checks it can plausibly affect,
63
+ plus the always-run criticals (the critical-path block above). Tell the user
64
+ which checks run and why.
65
+ 2. **Build** via the recorded build command; zip the result.
66
+ 3. **Ship:** `qaping publish-build <game.zip> --platform windows|macos` →
67
+ prints a `/b/<slug>` URL (temporary hosting; filing a playtest extends the
68
+ build through the round; each publish mints a NEW URL). A store-delivered
69
+ game files with its Steam store page, TestFlight public link, or App Store
70
+ page as `url` instead.
71
+ 4. **File** ONE `qaping_playtest` per batch of human checks a single session
72
+ can cover, `est_minutes` an honest sum of the steps — the service accepts
73
+ 5–30 minutes (10 is the standard session; outside that range the filing is
74
+ refused, not clamped), and the clock is play time: download/install happens
75
+ before it starts. The authored
76
+ `instructions` + `steps` must embed:
77
+ - the selected checks' steps, in play order (the plan's prose, verbatim
78
+ where possible);
79
+ - the previous round's findings as context — "last round reported X —
80
+ recheck" — so regressions get re-examined on every patch;
81
+ - the hardware-verification convention: when the game needs more than
82
+ keyboard+mouse, say so up front in `instructions` (playtesters self-select
83
+ before claiming) AND add an explicit step asking which input device was
84
+ actually used, with options — the answer comes back verified in
85
+ `steps_result`.
86
+ For a regression batch that knows exactly what it is testing, send
87
+ `questionnaire:'none'` so your steps are the only questions; leave the
88
+ standard questionnaire on for a first baseline round.
89
+ 5. **Wait:** `qaping_wait` until results arrive (see ground rules).
90
+ 6. **Report** from `qaping_results`:
91
+ - PR comment via `gh pr comment`: ONE verdict sentence first, then one line
92
+ per check — ✅/❌ with a recording timestamp ([mm:ss] from the transcript
93
+ where present; from watching the recording on Windows) — then credits
94
+ spent.
95
+ - One `gh issue create` per CONFIRMED bug: title, repro steps rebuilt from
96
+ the playtester's answers, a timestamp link into the recording. Issues are
97
+ drafts for the dev's own triage — never near-duplicates, never one issue
98
+ per symptom of the same bug.
99
+ - Update `QA-PLAN.md`: each run check's `last_verified` becomes this
100
+ build/date; a check changed because of this round records the round id as
101
+ `origin`.
102
+
103
+ ## PROMOTE — make the suite cheaper
104
+
105
+ When a check has passed and is mechanically assertable — a save file loads, a
106
+ config parses, an asset path resolves — offer to write a normal test in the
107
+ repo's own test suite. On the dev's yes: write the test, flip the check's
108
+ `rung` to `code`, keep the prose (it documents intent) and add a `test:` line
109
+ naming the test file. `rung: code` checks then run in the dev's own CI for
110
+ free — that is the point: every check starts human and gets cheaper over time.
111
+ Checks about feel, difficulty, or ambiguous visuals stay `rung: human`
112
+ permanently. Never delete or demote a human check without the dev agreeing.