@dichovsky/testrail-api-client 1.0.0 → 2.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.
Files changed (137) hide show
  1. package/README.md +22 -0
  2. package/dist/cli/auth.d.ts +21 -0
  3. package/dist/cli/auth.js +16 -0
  4. package/dist/cli/body.d.ts +42 -0
  5. package/dist/cli/body.js +89 -0
  6. package/dist/cli/dispatch.d.ts +16 -0
  7. package/dist/cli/dispatch.js +87 -0
  8. package/dist/cli/handler-context.d.ts +43 -0
  9. package/dist/cli/handler-context.js +2 -0
  10. package/dist/cli/handlers/case-write.d.ts +4 -0
  11. package/dist/cli/handlers/case-write.js +26 -0
  12. package/dist/cli/handlers/case.d.ts +4 -0
  13. package/dist/cli/handlers/case.js +11 -0
  14. package/dist/cli/handlers/milestone.d.ts +4 -0
  15. package/dist/cli/handlers/milestone.js +15 -0
  16. package/dist/cli/handlers/project.d.ts +4 -0
  17. package/dist/cli/handlers/project.js +11 -0
  18. package/dist/cli/handlers/result-write.d.ts +4 -0
  19. package/dist/cli/handlers/result-write.js +40 -0
  20. package/dist/cli/handlers/result.d.ts +3 -0
  21. package/dist/cli/handlers/result.js +11 -0
  22. package/dist/cli/handlers/run-write.d.ts +10 -0
  23. package/dist/cli/handlers/run-write.js +29 -0
  24. package/dist/cli/handlers/run.d.ts +4 -0
  25. package/dist/cli/handlers/run.js +15 -0
  26. package/dist/cli/handlers/suite.d.ts +4 -0
  27. package/dist/cli/handlers/suite.js +10 -0
  28. package/dist/cli/handlers/user.d.ts +4 -0
  29. package/dist/cli/handlers/user.js +11 -0
  30. package/dist/cli/ids.d.ts +6 -0
  31. package/dist/cli/ids.js +20 -0
  32. package/dist/cli/index.d.ts +3 -0
  33. package/dist/cli/index.js +198 -0
  34. package/dist/cli/install-skill.d.ts +35 -0
  35. package/dist/cli/install-skill.js +71 -0
  36. package/dist/cli/metadata.d.ts +37 -0
  37. package/dist/cli/metadata.js +151 -0
  38. package/dist/cli/output.d.ts +28 -0
  39. package/dist/cli/output.js +84 -0
  40. package/dist/cli.d.ts +1 -1
  41. package/dist/cli.js +1 -266
  42. package/dist/client-core.d.ts +16 -7
  43. package/dist/client-core.js +153 -27
  44. package/dist/client.d.ts +274 -118
  45. package/dist/client.js +404 -463
  46. package/dist/constants.d.ts +1 -0
  47. package/dist/constants.js +1 -0
  48. package/dist/errors.d.ts +11 -9
  49. package/dist/errors.js +12 -8
  50. package/dist/index.d.ts +4 -2
  51. package/dist/index.js +2 -1
  52. package/dist/modules/attachments.d.ts +19 -0
  53. package/dist/modules/attachments.js +64 -0
  54. package/dist/modules/cases.d.ts +13 -0
  55. package/dist/modules/cases.js +58 -0
  56. package/dist/modules/configurations.d.ts +14 -0
  57. package/dist/modules/configurations.js +37 -0
  58. package/dist/modules/datasets.d.ts +12 -0
  59. package/dist/modules/datasets.js +28 -0
  60. package/dist/modules/metadata.d.ts +14 -0
  61. package/dist/modules/metadata.js +31 -0
  62. package/dist/modules/milestones.d.ts +12 -0
  63. package/dist/modules/milestones.js +36 -0
  64. package/dist/modules/plans.d.ts +16 -0
  65. package/dist/modules/plans.js +59 -0
  66. package/dist/modules/projects.d.ts +36 -0
  67. package/dist/modules/projects.js +55 -0
  68. package/dist/modules/reports.d.ts +9 -0
  69. package/dist/modules/reports.js +16 -0
  70. package/dist/modules/results.d.ts +14 -0
  71. package/dist/modules/results.js +69 -0
  72. package/dist/modules/runs.d.ts +14 -0
  73. package/dist/modules/runs.js +57 -0
  74. package/dist/modules/sections.d.ts +16 -0
  75. package/dist/modules/sections.js +37 -0
  76. package/dist/modules/sharedSteps.d.ts +12 -0
  77. package/dist/modules/sharedSteps.js +28 -0
  78. package/dist/modules/suites.d.ts +37 -0
  79. package/dist/modules/suites.js +54 -0
  80. package/dist/modules/tests.d.ts +9 -0
  81. package/dist/modules/tests.js +25 -0
  82. package/dist/modules/users.d.ts +18 -0
  83. package/dist/modules/users.js +62 -0
  84. package/dist/modules/variables.d.ts +11 -0
  85. package/dist/modules/variables.js +24 -0
  86. package/dist/schemas.d.ts +544 -0
  87. package/dist/schemas.js +419 -0
  88. package/dist/types.d.ts +1 -55
  89. package/dist/utils.d.ts +2 -0
  90. package/dist/utils.js +4 -0
  91. package/package.json +23 -15
  92. package/skill/SKILL.md +395 -0
  93. package/src/cli/auth.ts +37 -0
  94. package/src/cli/body.ts +100 -0
  95. package/src/cli/dispatch.ts +91 -0
  96. package/src/cli/handler-context.ts +46 -0
  97. package/src/cli/handlers/case-write.ts +26 -0
  98. package/src/cli/handlers/case.ts +13 -0
  99. package/src/cli/handlers/milestone.ts +19 -0
  100. package/src/cli/handlers/project.ts +13 -0
  101. package/src/cli/handlers/result-write.ts +40 -0
  102. package/src/cli/handlers/result.ts +14 -0
  103. package/src/cli/handlers/run-write.ts +30 -0
  104. package/src/cli/handlers/run.ts +19 -0
  105. package/src/cli/handlers/suite.ts +12 -0
  106. package/src/cli/handlers/user.ts +13 -0
  107. package/src/cli/ids.ts +20 -0
  108. package/src/cli/index.ts +224 -0
  109. package/src/cli/install-skill.ts +89 -0
  110. package/src/cli/metadata.ts +194 -0
  111. package/src/cli/output.ts +96 -0
  112. package/src/cli.ts +1 -286
  113. package/src/client-core.ts +183 -67
  114. package/src/client.ts +414 -483
  115. package/src/constants.ts +1 -0
  116. package/src/errors.ts +18 -11
  117. package/src/index.ts +50 -8
  118. package/src/modules/attachments.ts +125 -0
  119. package/src/modules/cases.ts +78 -0
  120. package/src/modules/configurations.ts +68 -0
  121. package/src/modules/datasets.ts +44 -0
  122. package/src/modules/metadata.ts +63 -0
  123. package/src/modules/milestones.ts +54 -0
  124. package/src/modules/plans.ts +89 -0
  125. package/src/modules/projects.ts +67 -0
  126. package/src/modules/reports.ts +23 -0
  127. package/src/modules/results.ts +90 -0
  128. package/src/modules/runs.ts +70 -0
  129. package/src/modules/sections.ts +55 -0
  130. package/src/modules/sharedSteps.ts +44 -0
  131. package/src/modules/suites.ts +67 -0
  132. package/src/modules/tests.ts +28 -0
  133. package/src/modules/users.ts +87 -0
  134. package/src/modules/variables.ts +36 -0
  135. package/src/schemas.ts +551 -0
  136. package/src/types.ts +11 -60
  137. package/src/utils.ts +5 -0
package/skill/SKILL.md ADDED
@@ -0,0 +1,395 @@
1
+ ---
2
+ name: testrail-cli
3
+ description: Use the `testrail` CLI to query and write TestRail projects, suites, cases, runs, results, milestones, and users from the shell. Trigger when the user asks to look up, list, fetch, count, inspect, create, update, or publish TestRail entities, or when TESTRAIL_BASE_URL / TESTRAIL_EMAIL / TESTRAIL_API_KEY are set in the environment.
4
+ version: 2.1.0
5
+ license: MIT
6
+ homepage: https://github.com/dichovsky/testrail-api-client
7
+ ---
8
+
9
+ # `testrail` CLI
10
+
11
+ A zero-dependency Node CLI for the TestRail REST API. Covers query (`get`,
12
+ `list`) and write (`add`, `update`, `add-bulk`, `close`) operations across
13
+ projects, suites, cases, runs, results, milestones, and users.
14
+
15
+ This skill is designed for **coding agents** running shell commands. It is
16
+ not a TestRail user manual. For the browser UI, see TestRail's own docs.
17
+
18
+ ## When to use this skill
19
+
20
+ - The user mentions TestRail by name, or asks about test cases, test runs,
21
+ test results, or test plans in a TestRail context.
22
+ - `TESTRAIL_BASE_URL` / `TESTRAIL_EMAIL` / `TESTRAIL_API_KEY` are set in the
23
+ environment.
24
+ - The user wants to: look up, list, fetch, count, inspect, create, update,
25
+ or publish TestRail entities from the shell or from CI.
26
+
27
+ ## Install / verify
28
+
29
+ The CLI ships with the npm package `@dichovsky/testrail-api-client` and
30
+ exposes the `testrail` binary. Verify it is available:
31
+
32
+ ```bash
33
+ npx testrail --version
34
+ ```
35
+
36
+ If you installed the package locally, `npx testrail` runs the local copy
37
+ without a global install.
38
+
39
+ ## Authentication
40
+
41
+ The CLI requires three credentials. Provide them either as environment
42
+ variables (preferred — keeps secrets out of argv and shell history) or as
43
+ flags:
44
+
45
+ | Purpose | Env var | Flag |
46
+ | ------------- | ------------------- | ------------------ |
47
+ | TestRail URL | `TESTRAIL_BASE_URL` | `--base-url <url>` |
48
+ | Account email | `TESTRAIL_EMAIL` | `--email <email>` |
49
+ | API key | `TESTRAIL_API_KEY` | `--api-key <key>` |
50
+
51
+ If credentials are missing, the CLI exits 1 with `Error: Missing auth...`
52
+ on stderr. Never echo or log the API key.
53
+
54
+ ## Command surface
55
+
56
+ <!-- GENERATED:command-table -->
57
+ | Resource | Action | Path args | Body | Description |
58
+ | --- | --- | --- | --- | --- |
59
+ | project | get | `<project_id>` | — | Fetch a single project by ID |
60
+ | project | list | — | — | List all projects (paginated) |
61
+ | suite | get | `<suite_id>` | — | Fetch a single suite by ID |
62
+ | suite | list | — | — | List suites in a project |
63
+ | case | get | `<case_id>` | — | Fetch a single test case by ID |
64
+ | case | list | — | — | List cases in a project (optionally filtered by suite) |
65
+ | run | get | `<run_id>` | — | Fetch a single run by ID |
66
+ | run | list | — | — | List runs in a project (paginated) |
67
+ | result | list | — | — | List results for a run (paginated) |
68
+ | milestone | get | `<milestone_id>` | — | Fetch a single milestone by ID |
69
+ | milestone | list | — | — | List milestones in a project (paginated) |
70
+ | user | get | `<user_id>` | — | Fetch a single user by ID |
71
+ | user | list | — | — | List users (paginated) |
72
+ | case | add | `<section_id>` | `AddCasePayloadSchema` | Create a new test case under a section |
73
+ | case | update | `<case_id>` | `UpdateCasePayloadSchema` | Update an existing test case (partial fields) |
74
+ | run | add | `<project_id>` | `AddRunPayloadSchema` | Create a new test run in a project |
75
+ | run | close | `<run_id>` | — (no body) | Close a test run (no body) |
76
+ | result | add | `<run_id>` `<case_id>` | `AddResultPayloadSchema` | Record a single result for a case in a run |
77
+ | result | add-bulk | `<run_id>` | `AddResultsForCasesPayloadSchema` | Record multiple results for cases in one API call |
78
+ <!-- /GENERATED:command-table -->
79
+
80
+ ## Body input for write actions
81
+
82
+ For body-bearing write actions (all except `run close`), provide the JSON
83
+ payload via **exactly one** of:
84
+
85
+ ```bash
86
+ # (a) inline string — best for short payloads, agent-generated
87
+ testrail case add 5 --data '{"title":"New case"}'
88
+
89
+ # (b) file — best for large/repeated payloads, reviewable in git
90
+ testrail case add 5 --data-file payload.json
91
+
92
+ # (c) piped stdin — best for shell composition with jq / curl / etc.
93
+ echo '{"title":"New case"}' | testrail case add 5
94
+ ```
95
+
96
+ The CLI exits 1 if zero or more than one body source is provided.
97
+
98
+ ### `--dry-run`
99
+
100
+ Add `--dry-run` to validate the payload against the Zod schema and print
101
+ what _would_ be sent, without making an API call. Useful for verifying
102
+ payload shape before consuming TestRail rate limit.
103
+
104
+ ```bash
105
+ testrail case add 5 --data '{"title":"x"}' --dry-run
106
+ ```
107
+
108
+ ## Payload schemas
109
+
110
+ Each write action validates its body against a Zod schema with
111
+ `.passthrough()` — required fields must match types exactly (no
112
+ coercion; `"5"` is rejected where `5` is expected), and TestRail
113
+ `custom_*` fields pass through untouched.
114
+
115
+ <!-- GENERATED:payload-schemas -->
116
+ ### `AddCasePayloadSchema` (used by `case add`)
117
+
118
+ ```jsonc
119
+ {
120
+ "title": "string (required)",
121
+ "template_id": "number?",
122
+ "type_id": "number?",
123
+ "priority_id": "number?",
124
+ "estimate": "string?",
125
+ "milestone_id": "number?",
126
+ "refs": "string?",
127
+ "custom_fields": "Record<string, unknown>?"
128
+ }
129
+ ```
130
+
131
+ ### `UpdateCasePayloadSchema` (used by `case update`)
132
+
133
+ ```jsonc
134
+ {
135
+ "title": "string?",
136
+ "template_id": "number?",
137
+ "type_id": "number?",
138
+ "priority_id": "number?",
139
+ "estimate": "string?",
140
+ "milestone_id": "number?",
141
+ "refs": "string?",
142
+ "custom_fields": "Record<string, unknown>?"
143
+ }
144
+ ```
145
+
146
+ ### `AddRunPayloadSchema` (used by `run add`)
147
+
148
+ ```jsonc
149
+ {
150
+ "name": "string (required)",
151
+ "suite_id": "number?",
152
+ "description": "string?",
153
+ "milestone_id": "number?",
154
+ "assignedto_id": "number?",
155
+ "include_all": "boolean?",
156
+ "case_ids": "number[]?",
157
+ "refs": "string?"
158
+ }
159
+ ```
160
+
161
+ ### `AddResultPayloadSchema` (used by `result add`)
162
+
163
+ ```jsonc
164
+ {
165
+ "status_id": "number (required)",
166
+ "comment": "string?",
167
+ "version": "string?",
168
+ "elapsed": "string?",
169
+ "defects": "string?",
170
+ "assignedto_id": "number?",
171
+ "custom_fields": "Record<string, unknown>?"
172
+ }
173
+ ```
174
+
175
+ ### `AddResultsForCasesPayloadSchema` (used by `result add-bulk`)
176
+
177
+ ```jsonc
178
+ {
179
+ "results": "object[] (required)"
180
+ }
181
+ ```
182
+ <!-- /GENERATED:payload-schemas -->
183
+
184
+ For the authoritative type definitions, see `src/schemas.ts` in the
185
+ package source.
186
+
187
+ ## Output
188
+
189
+ By default, `testrail` emits pretty-printed JSON to stdout. Use `--format
190
+ table` for column-aligned human-readable output. Use `--quiet` to
191
+ suppress stdout entirely (rely on exit code).
192
+
193
+ ```bash
194
+ testrail project get 1 # JSON (default)
195
+ testrail project list --format table # Table
196
+ testrail run get 5 --quiet # Exit code 0/1 only
197
+ ```
198
+
199
+ ### Filtering output (preserve context budget)
200
+
201
+ The CLI emits the full JSON object for each entity. For list endpoints
202
+ with hundreds of items, that can blow the agent's context window. Filter
203
+ at the shell when possible:
204
+
205
+ **Preferred:** `jq` (if available — most dev/CI environments have it):
206
+
207
+ ```bash
208
+ testrail run get 5 | jq '.passed_count'
209
+ testrail case list --project-id 1 | jq '.[] | {id, title}'
210
+ ```
211
+
212
+ **Fallback:** Node one-liner (always available since the package itself
213
+ is a Node CLI):
214
+
215
+ ```bash
216
+ testrail run get 5 | node -e 'const d=JSON.parse(require("fs").readFileSync(0));console.log(d.passed_count)'
217
+ ```
218
+
219
+ ## Recipes
220
+
221
+ ### 1. Smoke-test auth & connectivity
222
+
223
+ ```bash
224
+ testrail user list --limit 1 --quiet && echo "auth OK" || echo "auth FAILED"
225
+ ```
226
+
227
+ Exit code 0 = creds resolve and TestRail responds; 1 = anything broken.
228
+
229
+ ### 2. Fetch a project
230
+
231
+ ```bash
232
+ testrail project get 5
233
+ ```
234
+
235
+ ### 3. List projects with pagination
236
+
237
+ ```bash
238
+ testrail project list --limit 25 --offset 0
239
+ ```
240
+
241
+ ### 4. List suites under a project
242
+
243
+ ```bash
244
+ testrail suite list --project-id 5
245
+ ```
246
+
247
+ ### 5. List cases in a specific suite
248
+
249
+ ```bash
250
+ testrail case list --project-id 5 --suite-id 12
251
+ ```
252
+
253
+ ### 6. Extract just the IDs from any list (generic pattern)
254
+
255
+ ```bash
256
+ testrail case list --project-id 5 | jq '.[].id'
257
+ ```
258
+
259
+ ### 7. Count pass/fail for a run
260
+
261
+ ```bash
262
+ testrail run get 42 | jq '{passed: .passed_count, failed: .failed_count}'
263
+ ```
264
+
265
+ ### 8. Page through a large result list
266
+
267
+ ```bash
268
+ offset=0
269
+ while true; do
270
+ page=$(testrail result list --run-id 100 --limit 100 --offset $offset)
271
+ count=$(echo "$page" | jq 'length')
272
+ [ "$count" -eq 0 ] && break
273
+ echo "$page" | jq -c '.[]'
274
+ offset=$((offset + count))
275
+ done
276
+ ```
277
+
278
+ ### 9. Author a new test case
279
+
280
+ ```bash
281
+ testrail case add 12 --data '{
282
+ "title": "Login page accepts SSO redirect",
283
+ "type_id": 1,
284
+ "priority_id": 3,
285
+ "refs": "JIRA-1234"
286
+ }'
287
+ ```
288
+
289
+ ### 10. Update a test case (partial fields)
290
+
291
+ ```bash
292
+ testrail case update 87 --data '{"title": "Renamed", "priority_id": 4}'
293
+ ```
294
+
295
+ ### 11. Create a CI test run
296
+
297
+ ```bash
298
+ RUN=$(testrail run add 5 --data '{
299
+ "name": "CI build #'"$CI_BUILD_NUMBER"'",
300
+ "include_all": false,
301
+ "case_ids": [42, 43, 44]
302
+ }')
303
+ RUN_ID=$(echo "$RUN" | jq '.id')
304
+ ```
305
+
306
+ ### 12. Publish bulk results from a CI run
307
+
308
+ ```bash
309
+ testrail result add-bulk "$RUN_ID" --data-file /tmp/results.json
310
+ ```
311
+
312
+ Where `/tmp/results.json` has shape:
313
+
314
+ ```json
315
+ {
316
+ "results": [
317
+ { "case_id": 42, "status_id": 1, "comment": "passed" },
318
+ { "case_id": 43, "status_id": 5, "comment": "failed: timeout" }
319
+ ]
320
+ }
321
+ ```
322
+
323
+ ### 13. Close a run when CI finishes
324
+
325
+ ```bash
326
+ testrail run close "$RUN_ID"
327
+ ```
328
+
329
+ ### 14. Validate a payload before sending (`--dry-run`)
330
+
331
+ ```bash
332
+ testrail result add 100 42 --data '{"status_id":1,"comment":"sanity check"}' --dry-run
333
+ ```
334
+
335
+ Prints the parsed payload + a `"dryRun": true` marker; no API call made.
336
+
337
+ ## Errors & exit codes
338
+
339
+ | Exit | Meaning |
340
+ | ---- | ---------------------------------------------------------------------------------- |
341
+ | `0` | Success |
342
+ | `1` | Any failure: bad auth, invalid args, validation, 4xx/5xx HTTP, rate limit, timeout |
343
+
344
+ Errors are written to stderr in the form `Error: <message>`. Common
345
+ causes:
346
+
347
+ - `Missing auth.` → env vars / flags not set.
348
+ - `<param> must be a positive integer` → bad path arg (e.g. `project get abc`).
349
+ - `Unknown resource '<x>'. Use: project, suite, case, run, result, milestone, user`
350
+ - `Unknown action '<a>' for <r>. Use: get, list, …`
351
+ - `Body required.` → write action invoked with no `--data` / `--data-file` / stdin.
352
+ - `Invalid JSON: …` → malformed body.
353
+ - `Payload validation failed: …` → body shape doesn't match the Zod schema.
354
+ - `TestRail API error: 404 Not Found …` → 4xx/5xx response from TestRail.
355
+
356
+ ## Limits & gotchas
357
+
358
+ - **Rate limit:** 100 requests / 60s by default (configurable on the
359
+ programmatic client; the CLI uses defaults). The CLI throws an error
360
+ rather than queueing on overflow.
361
+ - **GET cache:** GET responses are cached in-process for ~5 minutes by
362
+ default. POSTs invalidate the entire cache. Stale reads are possible
363
+ if the same `testrail` invocation re-fetches the same endpoint within
364
+ the TTL.
365
+ - **Retry:** 5xx responses, 429s, and network errors are retried with
366
+ exponential backoff (max 3 attempts). 4xx and timeout errors are not
367
+ retried.
368
+ - **No coercion on write payloads:** `"5"` is **not** silently converted
369
+ to `5`. This is intentional — catches agent template-substitution
370
+ bugs at the CLI boundary rather than the API call site.
371
+ - **`custom_*` fields:** Pass through `.passthrough()` schemas unchanged.
372
+ Field naming follows TestRail's `custom_<field-system-name>` convention.
373
+
374
+ ## When NOT to use this skill
375
+
376
+ - **Writing TypeScript/JavaScript code that imports the package.**
377
+ This skill documents the CLI surface only. For programmatic use,
378
+ read `README.md` and `CODEMAP.md` in the package — the programmatic
379
+ API exposes 101 methods, far beyond what the CLI covers.
380
+ - **Project / suite / section / milestone / user CRUD** beyond the 6
381
+ write actions listed above. Use the TestRail web UI for structural
382
+ setup (or the programmatic API).
383
+ - **Deletes** of any kind. The CLI deliberately does not expose `delete`
384
+ actions to keep agents away from irrecoverable operations.
385
+ - **Attachments** (upload/download). Not in the CLI surface; use the
386
+ programmatic API.
387
+ - **Browser/UI workflows.** This is a non-interactive CLI.
388
+
389
+ ## See also
390
+
391
+ - `README.md` — package install, programmatic API overview, configuration
392
+ - `CODEMAP.md` — every public method, type, error class, and constant
393
+ - `src/schemas.ts` — Zod payload schemas (source of truth)
394
+ - `BACKLOG.md` — deferred CLI/skill features tracked for future releases
395
+ - TestRail API docs: <https://support.testrail.com/hc/en-us/articles/7077083596436-Introduction-to-the-TestRail-API>
@@ -0,0 +1,37 @@
1
+ import type { TestRailConfig } from '../types.js';
2
+
3
+ export interface AuthFlags {
4
+ baseUrl: string | undefined;
5
+ email: string | undefined;
6
+ apiKey: string | undefined;
7
+ }
8
+
9
+ export interface AuthEnv {
10
+ TESTRAIL_BASE_URL?: string;
11
+ TESTRAIL_EMAIL?: string;
12
+ TESTRAIL_API_KEY?: string;
13
+ }
14
+
15
+ export type AuthResolution = { ok: true; config: TestRailConfig } | { ok: false; error: string };
16
+
17
+ export const MISSING_AUTH_MESSAGE =
18
+ 'Missing auth. Set TESTRAIL_BASE_URL, TESTRAIL_EMAIL, TESTRAIL_API_KEY or use --base-url, --email, --api-key flags.';
19
+
20
+ export function resolveAuth(flags: AuthFlags, env: AuthEnv): AuthResolution {
21
+ const baseUrl = flags.baseUrl ?? env.TESTRAIL_BASE_URL;
22
+ const email = flags.email ?? env.TESTRAIL_EMAIL;
23
+ const apiKey = flags.apiKey ?? env.TESTRAIL_API_KEY;
24
+
25
+ if (
26
+ baseUrl === undefined ||
27
+ baseUrl === '' ||
28
+ email === undefined ||
29
+ email === '' ||
30
+ apiKey === undefined ||
31
+ apiKey === ''
32
+ ) {
33
+ return { ok: false, error: MISSING_AUTH_MESSAGE };
34
+ }
35
+
36
+ return { ok: true, config: { baseUrl, email, apiKey } };
37
+ }
@@ -0,0 +1,100 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import type { z } from 'zod';
3
+ import type { BodyInput } from './handler-context.js';
4
+
5
+ /**
6
+ * Source of a parsed body. Reported alongside the resolution result so
7
+ * handlers (or callers writing recipes) can log which input mechanism the
8
+ * agent actually used.
9
+ */
10
+ export type BodySource = 'data' | 'file' | 'stdin';
11
+
12
+ export type BodyResolution<T> = { ok: true; payload: T; source: BodySource } | { ok: false; error: string };
13
+
14
+ /**
15
+ * Resolve a write-action body from one of three mutually-exclusive sources:
16
+ *
17
+ * - `--data <json-string>` (provided via `BodyInput.dataFlag`)
18
+ * - `--data-file <path>` (provided via `BodyInput.dataFileFlag`; read via
19
+ * readFileSync so failures surface as a structured `ok: false` rather than
20
+ * crashing the CLI)
21
+ * - stdin (provided via `BodyInput.readStdin` thunk; the caller is
22
+ * responsible for non-TTY detection — only set the thunk when stdin is
23
+ * piped. The resolver invokes the thunk *only* when stdin is the
24
+ * selected source, so read actions and no-body writes never drain it.)
25
+ *
26
+ * After source resolution: JSON-parse the raw string, then validate against
27
+ * the supplied Zod schema. No coercion is applied (Q8 lock from
28
+ * SKILL-PLAN.md) — wrong types are rejected immediately so agent payload
29
+ * bugs surface at the CLI boundary rather than the API call site.
30
+ *
31
+ * Typed via `S extends z.ZodTypeAny` so the inferred payload type carries
32
+ * through to the caller — `resolveBody(input, AddCasePayloadSchema)` returns
33
+ * `BodyResolution<AddCasePayload>`, not `BodyResolution<unknown>`.
34
+ *
35
+ * @param input raw inputs from the CLI argv + stdin reader
36
+ * @param schema Zod schema for the expected payload shape
37
+ */
38
+ export function resolveBody<S extends z.ZodTypeAny>(input: BodyInput, schema: S): BodyResolution<z.infer<S>> {
39
+ const sources = [
40
+ input.dataFlag !== undefined ? 'data' : null,
41
+ input.dataFileFlag !== undefined ? 'file' : null,
42
+ input.readStdin !== undefined ? 'stdin' : null,
43
+ ].filter((s): s is BodySource => s !== null);
44
+
45
+ if (sources.length === 0) {
46
+ return {
47
+ ok: false,
48
+ error: 'Body required. Pass a JSON payload via --data <json>, --data-file <path>, or stdin pipe.',
49
+ };
50
+ }
51
+ if (sources.length > 1) {
52
+ return {
53
+ ok: false,
54
+ error: `Multiple body sources provided (${sources.join(', ')}). Use exactly one of --data, --data-file, or stdin.`,
55
+ };
56
+ }
57
+
58
+ let raw: string;
59
+ let source: BodySource;
60
+ if (input.dataFlag !== undefined) {
61
+ raw = input.dataFlag;
62
+ source = 'data';
63
+ } else if (input.dataFileFlag !== undefined) {
64
+ try {
65
+ raw = readFileSync(input.dataFileFlag, 'utf-8');
66
+ } catch (e) {
67
+ return {
68
+ ok: false,
69
+ error: `Cannot read --data-file '${input.dataFileFlag}': ${e instanceof Error ? e.message : String(e)}`,
70
+ };
71
+ }
72
+ source = 'file';
73
+ } else {
74
+ // stdin is the only remaining source. Invoke the thunk now (and only
75
+ // now) so the underlying readFileSync(0) call happens lazily.
76
+ try {
77
+ raw = (input.readStdin as () => string)();
78
+ } catch (e) {
79
+ return {
80
+ ok: false,
81
+ error: `Cannot read stdin: ${e instanceof Error ? e.message : String(e)}`,
82
+ };
83
+ }
84
+ source = 'stdin';
85
+ }
86
+
87
+ let parsed: unknown;
88
+ try {
89
+ parsed = JSON.parse(raw);
90
+ } catch (e) {
91
+ return { ok: false, error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` };
92
+ }
93
+
94
+ const result = schema.safeParse(parsed);
95
+ if (!result.success) {
96
+ return { ok: false, error: `Payload validation failed: ${result.error.message}` };
97
+ }
98
+
99
+ return { ok: true, payload: result.data, source };
100
+ }
@@ -0,0 +1,91 @@
1
+ import type { Handler } from './handler-context.js';
2
+ import { handleProjectGet, handleProjectList } from './handlers/project.js';
3
+ import { handleSuiteGet, handleSuiteList } from './handlers/suite.js';
4
+ import { handleCaseGet, handleCaseList } from './handlers/case.js';
5
+ import { handleCaseAdd, handleCaseUpdate } from './handlers/case-write.js';
6
+ import { handleRunGet, handleRunList } from './handlers/run.js';
7
+ import { handleRunAdd, handleRunClose } from './handlers/run-write.js';
8
+ import { handleResultList } from './handlers/result.js';
9
+ import { handleResultAdd, handleResultAddBulk } from './handlers/result-write.js';
10
+ import { handleMilestoneGet, handleMilestoneList } from './handlers/milestone.js';
11
+ import { handleUserGet, handleUserList } from './handlers/user.js';
12
+
13
+ /**
14
+ * Single source of truth: every supported resource:action mapped to its handler.
15
+ * Adding an action is a one-line change here — no parallel registry to keep in
16
+ * sync. `dispatch()` derives both the known-resources list and the
17
+ * known-actions-per-resource list from this map's keys, so error wording stays
18
+ * accurate even as the map evolves.
19
+ *
20
+ * Keys are inserted in the canonical display order (`get` before `list`, etc.);
21
+ * JavaScript object iteration order is insertion-order-preserving for string
22
+ * keys, which keeps error messages stable.
23
+ */
24
+ const HANDLERS: Record<string, Handler> = {
25
+ 'project:get': handleProjectGet,
26
+ 'project:list': handleProjectList,
27
+ 'suite:get': handleSuiteGet,
28
+ 'suite:list': handleSuiteList,
29
+ 'case:get': handleCaseGet,
30
+ 'case:list': handleCaseList,
31
+ 'case:add': handleCaseAdd,
32
+ 'case:update': handleCaseUpdate,
33
+ 'run:get': handleRunGet,
34
+ 'run:list': handleRunList,
35
+ 'run:add': handleRunAdd,
36
+ 'run:close': handleRunClose,
37
+ 'result:list': handleResultList,
38
+ 'result:add': handleResultAdd,
39
+ 'result:add-bulk': handleResultAddBulk,
40
+ 'milestone:get': handleMilestoneGet,
41
+ 'milestone:list': handleMilestoneList,
42
+ 'user:get': handleUserGet,
43
+ 'user:list': handleUserList,
44
+ };
45
+
46
+ const RESOURCES: Record<string, readonly string[]> = (() => {
47
+ const grouped: Record<string, string[]> = {};
48
+ for (const key of Object.keys(HANDLERS)) {
49
+ const [resource, action] = key.split(':');
50
+ if (resource === undefined || action === undefined) continue;
51
+ const existing = grouped[resource];
52
+ if (existing === undefined) {
53
+ grouped[resource] = [action];
54
+ } else {
55
+ existing.push(action);
56
+ }
57
+ }
58
+ return grouped;
59
+ })();
60
+
61
+ export type DispatchResult = { ok: true; handler: Handler } | { ok: false; error: string };
62
+
63
+ /**
64
+ * Returns every registered `resource:action` key. Used by the
65
+ * metadata↔dispatch consistency tests to catch handlers added without a
66
+ * matching metadata entry — the inverse of the metadata-first check.
67
+ */
68
+ export function getRegisteredActions(): readonly string[] {
69
+ return Object.keys(HANDLERS);
70
+ }
71
+
72
+ export function dispatch(resource: string, action: string): DispatchResult {
73
+ const actions = RESOURCES[resource];
74
+ if (actions === undefined) {
75
+ return {
76
+ ok: false,
77
+ error: `Unknown resource '${resource}'. Use: ${Object.keys(RESOURCES).join(', ')}`,
78
+ };
79
+ }
80
+ if (!actions.includes(action)) {
81
+ return {
82
+ ok: false,
83
+ error: `Unknown action '${action}' for ${resource}. Use: ${actions.join(', ')}`,
84
+ };
85
+ }
86
+ const handler = HANDLERS[`${resource}:${action}`];
87
+ if (handler === undefined) {
88
+ return { ok: false, error: `No handler registered for ${resource}:${action}` };
89
+ }
90
+ return { ok: true, handler };
91
+ }
@@ -0,0 +1,46 @@
1
+ import type { TestRailClient } from '../client.js';
2
+
3
+ /**
4
+ * Parsed CLI argument bundle passed to every handler.
5
+ *
6
+ * `pathParams` is the slice of positional args after `[resource, action]` —
7
+ * read handlers consume `pathParams[0]` (the single id), write handlers may
8
+ * consume multiple (e.g., `result add <run_id> <case_id>` uses [0] and [1]).
9
+ */
10
+ export interface HandlerArgs {
11
+ pathParams: readonly string[];
12
+ projectId?: string;
13
+ suiteId?: string;
14
+ runId?: string;
15
+ caseId?: string;
16
+ limit?: string;
17
+ offset?: string;
18
+ }
19
+
20
+ /**
21
+ * Raw inputs for the body-source resolver. The handler decides whether to
22
+ * consume any of these (write handlers do; read handlers ignore). When all
23
+ * three are absent for a write action, the resolver emits a "body required"
24
+ * error.
25
+ *
26
+ * `readStdin` is a thunk rather than pre-read contents so stdin is *only*
27
+ * drained when the resolver actually selects it as the body source. Read
28
+ * actions and no-body writes (`run close`) never invoke it — avoiding the
29
+ * "tail -f | testrail run close" hang and the cost of slurping a large
30
+ * redirected stdin that the handler will throw away.
31
+ */
32
+ export interface BodyInput {
33
+ dataFlag?: string;
34
+ dataFileFlag?: string;
35
+ readStdin?: () => string;
36
+ }
37
+
38
+ export interface HandlerContext {
39
+ client: TestRailClient;
40
+ args: HandlerArgs;
41
+ bodyInput: BodyInput;
42
+ dryRun: boolean;
43
+ out: (data: unknown) => void;
44
+ }
45
+
46
+ export type Handler = (ctx: HandlerContext) => Promise<void>;
@@ -0,0 +1,26 @@
1
+ import type { HandlerContext } from '../handler-context.js';
2
+ import { parseId } from '../ids.js';
3
+ import { resolveBody } from '../body.js';
4
+ import { AddCasePayloadSchema, UpdateCasePayloadSchema } from '../../schemas.js';
5
+
6
+ export async function handleCaseAdd(ctx: HandlerContext): Promise<void> {
7
+ const sectionId = parseId(ctx.args.pathParams[0], 'section_id');
8
+ const body = resolveBody(ctx.bodyInput, AddCasePayloadSchema);
9
+ if (!body.ok) throw new Error(body.error);
10
+ if (ctx.dryRun) {
11
+ ctx.out({ dryRun: true, action: 'case add', sectionId, payload: body.payload, source: body.source });
12
+ return;
13
+ }
14
+ ctx.out(await ctx.client.addCase(sectionId, body.payload));
15
+ }
16
+
17
+ export async function handleCaseUpdate(ctx: HandlerContext): Promise<void> {
18
+ const caseId = parseId(ctx.args.pathParams[0], 'case_id');
19
+ const body = resolveBody(ctx.bodyInput, UpdateCasePayloadSchema);
20
+ if (!body.ok) throw new Error(body.error);
21
+ if (ctx.dryRun) {
22
+ ctx.out({ dryRun: true, action: 'case update', caseId, payload: body.payload, source: body.source });
23
+ return;
24
+ }
25
+ ctx.out(await ctx.client.updateCase(caseId, body.payload));
26
+ }