@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.
- package/README.md +22 -0
- package/dist/cli/auth.d.ts +21 -0
- package/dist/cli/auth.js +16 -0
- package/dist/cli/body.d.ts +42 -0
- package/dist/cli/body.js +89 -0
- package/dist/cli/dispatch.d.ts +16 -0
- package/dist/cli/dispatch.js +87 -0
- package/dist/cli/handler-context.d.ts +43 -0
- package/dist/cli/handler-context.js +2 -0
- package/dist/cli/handlers/case-write.d.ts +4 -0
- package/dist/cli/handlers/case-write.js +26 -0
- package/dist/cli/handlers/case.d.ts +4 -0
- package/dist/cli/handlers/case.js +11 -0
- package/dist/cli/handlers/milestone.d.ts +4 -0
- package/dist/cli/handlers/milestone.js +15 -0
- package/dist/cli/handlers/project.d.ts +4 -0
- package/dist/cli/handlers/project.js +11 -0
- package/dist/cli/handlers/result-write.d.ts +4 -0
- package/dist/cli/handlers/result-write.js +40 -0
- package/dist/cli/handlers/result.d.ts +3 -0
- package/dist/cli/handlers/result.js +11 -0
- package/dist/cli/handlers/run-write.d.ts +10 -0
- package/dist/cli/handlers/run-write.js +29 -0
- package/dist/cli/handlers/run.d.ts +4 -0
- package/dist/cli/handlers/run.js +15 -0
- package/dist/cli/handlers/suite.d.ts +4 -0
- package/dist/cli/handlers/suite.js +10 -0
- package/dist/cli/handlers/user.d.ts +4 -0
- package/dist/cli/handlers/user.js +11 -0
- package/dist/cli/ids.d.ts +6 -0
- package/dist/cli/ids.js +20 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +198 -0
- package/dist/cli/install-skill.d.ts +35 -0
- package/dist/cli/install-skill.js +71 -0
- package/dist/cli/metadata.d.ts +37 -0
- package/dist/cli/metadata.js +151 -0
- package/dist/cli/output.d.ts +28 -0
- package/dist/cli/output.js +84 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -266
- package/dist/client-core.d.ts +16 -7
- package/dist/client-core.js +153 -27
- package/dist/client.d.ts +274 -118
- package/dist/client.js +404 -463
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/errors.d.ts +11 -9
- package/dist/errors.js +12 -8
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/modules/attachments.d.ts +19 -0
- package/dist/modules/attachments.js +64 -0
- package/dist/modules/cases.d.ts +13 -0
- package/dist/modules/cases.js +58 -0
- package/dist/modules/configurations.d.ts +14 -0
- package/dist/modules/configurations.js +37 -0
- package/dist/modules/datasets.d.ts +12 -0
- package/dist/modules/datasets.js +28 -0
- package/dist/modules/metadata.d.ts +14 -0
- package/dist/modules/metadata.js +31 -0
- package/dist/modules/milestones.d.ts +12 -0
- package/dist/modules/milestones.js +36 -0
- package/dist/modules/plans.d.ts +16 -0
- package/dist/modules/plans.js +59 -0
- package/dist/modules/projects.d.ts +36 -0
- package/dist/modules/projects.js +55 -0
- package/dist/modules/reports.d.ts +9 -0
- package/dist/modules/reports.js +16 -0
- package/dist/modules/results.d.ts +14 -0
- package/dist/modules/results.js +69 -0
- package/dist/modules/runs.d.ts +14 -0
- package/dist/modules/runs.js +57 -0
- package/dist/modules/sections.d.ts +16 -0
- package/dist/modules/sections.js +37 -0
- package/dist/modules/sharedSteps.d.ts +12 -0
- package/dist/modules/sharedSteps.js +28 -0
- package/dist/modules/suites.d.ts +37 -0
- package/dist/modules/suites.js +54 -0
- package/dist/modules/tests.d.ts +9 -0
- package/dist/modules/tests.js +25 -0
- package/dist/modules/users.d.ts +18 -0
- package/dist/modules/users.js +62 -0
- package/dist/modules/variables.d.ts +11 -0
- package/dist/modules/variables.js +24 -0
- package/dist/schemas.d.ts +544 -0
- package/dist/schemas.js +419 -0
- package/dist/types.d.ts +1 -55
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +4 -0
- package/package.json +23 -15
- package/skill/SKILL.md +395 -0
- package/src/cli/auth.ts +37 -0
- package/src/cli/body.ts +100 -0
- package/src/cli/dispatch.ts +91 -0
- package/src/cli/handler-context.ts +46 -0
- package/src/cli/handlers/case-write.ts +26 -0
- package/src/cli/handlers/case.ts +13 -0
- package/src/cli/handlers/milestone.ts +19 -0
- package/src/cli/handlers/project.ts +13 -0
- package/src/cli/handlers/result-write.ts +40 -0
- package/src/cli/handlers/result.ts +14 -0
- package/src/cli/handlers/run-write.ts +30 -0
- package/src/cli/handlers/run.ts +19 -0
- package/src/cli/handlers/suite.ts +12 -0
- package/src/cli/handlers/user.ts +13 -0
- package/src/cli/ids.ts +20 -0
- package/src/cli/index.ts +224 -0
- package/src/cli/install-skill.ts +89 -0
- package/src/cli/metadata.ts +194 -0
- package/src/cli/output.ts +96 -0
- package/src/cli.ts +1 -286
- package/src/client-core.ts +183 -67
- package/src/client.ts +414 -483
- package/src/constants.ts +1 -0
- package/src/errors.ts +18 -11
- package/src/index.ts +50 -8
- package/src/modules/attachments.ts +125 -0
- package/src/modules/cases.ts +78 -0
- package/src/modules/configurations.ts +68 -0
- package/src/modules/datasets.ts +44 -0
- package/src/modules/metadata.ts +63 -0
- package/src/modules/milestones.ts +54 -0
- package/src/modules/plans.ts +89 -0
- package/src/modules/projects.ts +67 -0
- package/src/modules/reports.ts +23 -0
- package/src/modules/results.ts +90 -0
- package/src/modules/runs.ts +70 -0
- package/src/modules/sections.ts +55 -0
- package/src/modules/sharedSteps.ts +44 -0
- package/src/modules/suites.ts +67 -0
- package/src/modules/tests.ts +28 -0
- package/src/modules/users.ts +87 -0
- package/src/modules/variables.ts +36 -0
- package/src/schemas.ts +551 -0
- package/src/types.ts +11 -60
- package/src/utils.ts +5 -0
package/README.md
CHANGED
|
@@ -55,6 +55,26 @@ A comprehensive, type-safe TypeScript/JavaScript client for the [TestRail API](h
|
|
|
55
55
|
npm install @dichovsky/testrail-api-client
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
## Using with AI Coding Agents
|
|
59
|
+
|
|
60
|
+
This package ships a Claude Code skill at `skill/SKILL.md` that teaches coding agents how to use the bundled `testrail` CLI. Install it into your project (or globally) so Claude Code auto-loads it whenever you ask the agent to query or write TestRail entities.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Install into the current project: ./.claude/skills/testrail-cli/SKILL.md
|
|
64
|
+
npx testrail install-skill
|
|
65
|
+
|
|
66
|
+
# Or globally: ~/.claude/skills/testrail-cli/SKILL.md
|
|
67
|
+
npx testrail install-skill --global
|
|
68
|
+
|
|
69
|
+
# Overwrite an existing install
|
|
70
|
+
npx testrail install-skill --force
|
|
71
|
+
|
|
72
|
+
# Just print the bundled SKILL.md path (for scripting / vendoring)
|
|
73
|
+
npx testrail install-skill --print-path
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The skill description triggers auto-load when an agent's prompt mentions TestRail entities (projects, suites, cases, runs, results, milestones, users) or when `TESTRAIL_BASE_URL` / `TESTRAIL_EMAIL` / `TESTRAIL_API_KEY` are set in the environment. The bundled CLI itself supports both read (`get`, `list`) and write (`add`, `update`, `add-bulk`, `close`) operations — see `skill/SKILL.md` for the complete command surface and recipes.
|
|
77
|
+
|
|
58
78
|
## Quick Start
|
|
59
79
|
|
|
60
80
|
```typescript
|
|
@@ -356,6 +376,8 @@ const priorities = await client.getPriorities();
|
|
|
356
376
|
const milestones = await client.getMilestones(projectId);
|
|
357
377
|
```
|
|
358
378
|
|
|
379
|
+
Advanced usage: the client also exposes domain modules such as `client.metadata` for organization, but the supported/documented usage pattern remains the top-level `client.getX()` methods.
|
|
380
|
+
|
|
359
381
|
### Caching
|
|
360
382
|
|
|
361
383
|
The client automatically caches GET requests to improve performance:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TestRailConfig } from '../types.js';
|
|
2
|
+
export interface AuthFlags {
|
|
3
|
+
baseUrl: string | undefined;
|
|
4
|
+
email: string | undefined;
|
|
5
|
+
apiKey: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface AuthEnv {
|
|
8
|
+
TESTRAIL_BASE_URL?: string;
|
|
9
|
+
TESTRAIL_EMAIL?: string;
|
|
10
|
+
TESTRAIL_API_KEY?: string;
|
|
11
|
+
}
|
|
12
|
+
export type AuthResolution = {
|
|
13
|
+
ok: true;
|
|
14
|
+
config: TestRailConfig;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
error: string;
|
|
18
|
+
};
|
|
19
|
+
export declare const MISSING_AUTH_MESSAGE = "Missing auth. Set TESTRAIL_BASE_URL, TESTRAIL_EMAIL, TESTRAIL_API_KEY or use --base-url, --email, --api-key flags.";
|
|
20
|
+
export declare function resolveAuth(flags: AuthFlags, env: AuthEnv): AuthResolution;
|
|
21
|
+
//# sourceMappingURL=auth.d.ts.map
|
package/dist/cli/auth.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const MISSING_AUTH_MESSAGE = 'Missing auth. Set TESTRAIL_BASE_URL, TESTRAIL_EMAIL, TESTRAIL_API_KEY or use --base-url, --email, --api-key flags.';
|
|
2
|
+
export function resolveAuth(flags, env) {
|
|
3
|
+
const baseUrl = flags.baseUrl ?? env.TESTRAIL_BASE_URL;
|
|
4
|
+
const email = flags.email ?? env.TESTRAIL_EMAIL;
|
|
5
|
+
const apiKey = flags.apiKey ?? env.TESTRAIL_API_KEY;
|
|
6
|
+
if (baseUrl === undefined ||
|
|
7
|
+
baseUrl === '' ||
|
|
8
|
+
email === undefined ||
|
|
9
|
+
email === '' ||
|
|
10
|
+
apiKey === undefined ||
|
|
11
|
+
apiKey === '') {
|
|
12
|
+
return { ok: false, error: MISSING_AUTH_MESSAGE };
|
|
13
|
+
}
|
|
14
|
+
return { ok: true, config: { baseUrl, email, apiKey } };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { BodyInput } from './handler-context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Source of a parsed body. Reported alongside the resolution result so
|
|
5
|
+
* handlers (or callers writing recipes) can log which input mechanism the
|
|
6
|
+
* agent actually used.
|
|
7
|
+
*/
|
|
8
|
+
export type BodySource = 'data' | 'file' | 'stdin';
|
|
9
|
+
export type BodyResolution<T> = {
|
|
10
|
+
ok: true;
|
|
11
|
+
payload: T;
|
|
12
|
+
source: BodySource;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a write-action body from one of three mutually-exclusive sources:
|
|
19
|
+
*
|
|
20
|
+
* - `--data <json-string>` (provided via `BodyInput.dataFlag`)
|
|
21
|
+
* - `--data-file <path>` (provided via `BodyInput.dataFileFlag`; read via
|
|
22
|
+
* readFileSync so failures surface as a structured `ok: false` rather than
|
|
23
|
+
* crashing the CLI)
|
|
24
|
+
* - stdin (provided via `BodyInput.readStdin` thunk; the caller is
|
|
25
|
+
* responsible for non-TTY detection — only set the thunk when stdin is
|
|
26
|
+
* piped. The resolver invokes the thunk *only* when stdin is the
|
|
27
|
+
* selected source, so read actions and no-body writes never drain it.)
|
|
28
|
+
*
|
|
29
|
+
* After source resolution: JSON-parse the raw string, then validate against
|
|
30
|
+
* the supplied Zod schema. No coercion is applied (Q8 lock from
|
|
31
|
+
* SKILL-PLAN.md) — wrong types are rejected immediately so agent payload
|
|
32
|
+
* bugs surface at the CLI boundary rather than the API call site.
|
|
33
|
+
*
|
|
34
|
+
* Typed via `S extends z.ZodTypeAny` so the inferred payload type carries
|
|
35
|
+
* through to the caller — `resolveBody(input, AddCasePayloadSchema)` returns
|
|
36
|
+
* `BodyResolution<AddCasePayload>`, not `BodyResolution<unknown>`.
|
|
37
|
+
*
|
|
38
|
+
* @param input raw inputs from the CLI argv + stdin reader
|
|
39
|
+
* @param schema Zod schema for the expected payload shape
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveBody<S extends z.ZodTypeAny>(input: BodyInput, schema: S): BodyResolution<z.infer<S>>;
|
|
42
|
+
//# sourceMappingURL=body.d.ts.map
|
package/dist/cli/body.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a write-action body from one of three mutually-exclusive sources:
|
|
4
|
+
*
|
|
5
|
+
* - `--data <json-string>` (provided via `BodyInput.dataFlag`)
|
|
6
|
+
* - `--data-file <path>` (provided via `BodyInput.dataFileFlag`; read via
|
|
7
|
+
* readFileSync so failures surface as a structured `ok: false` rather than
|
|
8
|
+
* crashing the CLI)
|
|
9
|
+
* - stdin (provided via `BodyInput.readStdin` thunk; the caller is
|
|
10
|
+
* responsible for non-TTY detection — only set the thunk when stdin is
|
|
11
|
+
* piped. The resolver invokes the thunk *only* when stdin is the
|
|
12
|
+
* selected source, so read actions and no-body writes never drain it.)
|
|
13
|
+
*
|
|
14
|
+
* After source resolution: JSON-parse the raw string, then validate against
|
|
15
|
+
* the supplied Zod schema. No coercion is applied (Q8 lock from
|
|
16
|
+
* SKILL-PLAN.md) — wrong types are rejected immediately so agent payload
|
|
17
|
+
* bugs surface at the CLI boundary rather than the API call site.
|
|
18
|
+
*
|
|
19
|
+
* Typed via `S extends z.ZodTypeAny` so the inferred payload type carries
|
|
20
|
+
* through to the caller — `resolveBody(input, AddCasePayloadSchema)` returns
|
|
21
|
+
* `BodyResolution<AddCasePayload>`, not `BodyResolution<unknown>`.
|
|
22
|
+
*
|
|
23
|
+
* @param input raw inputs from the CLI argv + stdin reader
|
|
24
|
+
* @param schema Zod schema for the expected payload shape
|
|
25
|
+
*/
|
|
26
|
+
export function resolveBody(input, schema) {
|
|
27
|
+
const sources = [
|
|
28
|
+
input.dataFlag !== undefined ? 'data' : null,
|
|
29
|
+
input.dataFileFlag !== undefined ? 'file' : null,
|
|
30
|
+
input.readStdin !== undefined ? 'stdin' : null,
|
|
31
|
+
].filter((s) => s !== null);
|
|
32
|
+
if (sources.length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: 'Body required. Pass a JSON payload via --data <json>, --data-file <path>, or stdin pipe.',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (sources.length > 1) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: `Multiple body sources provided (${sources.join(', ')}). Use exactly one of --data, --data-file, or stdin.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
let raw;
|
|
45
|
+
let source;
|
|
46
|
+
if (input.dataFlag !== undefined) {
|
|
47
|
+
raw = input.dataFlag;
|
|
48
|
+
source = 'data';
|
|
49
|
+
}
|
|
50
|
+
else if (input.dataFileFlag !== undefined) {
|
|
51
|
+
try {
|
|
52
|
+
raw = readFileSync(input.dataFileFlag, 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: `Cannot read --data-file '${input.dataFileFlag}': ${e instanceof Error ? e.message : String(e)}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
source = 'file';
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// stdin is the only remaining source. Invoke the thunk now (and only
|
|
64
|
+
// now) so the underlying readFileSync(0) call happens lazily.
|
|
65
|
+
try {
|
|
66
|
+
raw = input.readStdin();
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
error: `Cannot read stdin: ${e instanceof Error ? e.message : String(e)}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
source = 'stdin';
|
|
75
|
+
}
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return { ok: false, error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
82
|
+
}
|
|
83
|
+
const result = schema.safeParse(parsed);
|
|
84
|
+
if (!result.success) {
|
|
85
|
+
return { ok: false, error: `Payload validation failed: ${result.error.message}` };
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, payload: result.data, source };
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=body.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Handler } from './handler-context.js';
|
|
2
|
+
export type DispatchResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
handler: Handler;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
error: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Returns every registered `resource:action` key. Used by the
|
|
11
|
+
* metadata↔dispatch consistency tests to catch handlers added without a
|
|
12
|
+
* matching metadata entry — the inverse of the metadata-first check.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getRegisteredActions(): readonly string[];
|
|
15
|
+
export declare function dispatch(resource: string, action: string): DispatchResult;
|
|
16
|
+
//# sourceMappingURL=dispatch.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { handleProjectGet, handleProjectList } from './handlers/project.js';
|
|
2
|
+
import { handleSuiteGet, handleSuiteList } from './handlers/suite.js';
|
|
3
|
+
import { handleCaseGet, handleCaseList } from './handlers/case.js';
|
|
4
|
+
import { handleCaseAdd, handleCaseUpdate } from './handlers/case-write.js';
|
|
5
|
+
import { handleRunGet, handleRunList } from './handlers/run.js';
|
|
6
|
+
import { handleRunAdd, handleRunClose } from './handlers/run-write.js';
|
|
7
|
+
import { handleResultList } from './handlers/result.js';
|
|
8
|
+
import { handleResultAdd, handleResultAddBulk } from './handlers/result-write.js';
|
|
9
|
+
import { handleMilestoneGet, handleMilestoneList } from './handlers/milestone.js';
|
|
10
|
+
import { handleUserGet, handleUserList } from './handlers/user.js';
|
|
11
|
+
/**
|
|
12
|
+
* Single source of truth: every supported resource:action mapped to its handler.
|
|
13
|
+
* Adding an action is a one-line change here — no parallel registry to keep in
|
|
14
|
+
* sync. `dispatch()` derives both the known-resources list and the
|
|
15
|
+
* known-actions-per-resource list from this map's keys, so error wording stays
|
|
16
|
+
* accurate even as the map evolves.
|
|
17
|
+
*
|
|
18
|
+
* Keys are inserted in the canonical display order (`get` before `list`, etc.);
|
|
19
|
+
* JavaScript object iteration order is insertion-order-preserving for string
|
|
20
|
+
* keys, which keeps error messages stable.
|
|
21
|
+
*/
|
|
22
|
+
const HANDLERS = {
|
|
23
|
+
'project:get': handleProjectGet,
|
|
24
|
+
'project:list': handleProjectList,
|
|
25
|
+
'suite:get': handleSuiteGet,
|
|
26
|
+
'suite:list': handleSuiteList,
|
|
27
|
+
'case:get': handleCaseGet,
|
|
28
|
+
'case:list': handleCaseList,
|
|
29
|
+
'case:add': handleCaseAdd,
|
|
30
|
+
'case:update': handleCaseUpdate,
|
|
31
|
+
'run:get': handleRunGet,
|
|
32
|
+
'run:list': handleRunList,
|
|
33
|
+
'run:add': handleRunAdd,
|
|
34
|
+
'run:close': handleRunClose,
|
|
35
|
+
'result:list': handleResultList,
|
|
36
|
+
'result:add': handleResultAdd,
|
|
37
|
+
'result:add-bulk': handleResultAddBulk,
|
|
38
|
+
'milestone:get': handleMilestoneGet,
|
|
39
|
+
'milestone:list': handleMilestoneList,
|
|
40
|
+
'user:get': handleUserGet,
|
|
41
|
+
'user:list': handleUserList,
|
|
42
|
+
};
|
|
43
|
+
const RESOURCES = (() => {
|
|
44
|
+
const grouped = {};
|
|
45
|
+
for (const key of Object.keys(HANDLERS)) {
|
|
46
|
+
const [resource, action] = key.split(':');
|
|
47
|
+
if (resource === undefined || action === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
const existing = grouped[resource];
|
|
50
|
+
if (existing === undefined) {
|
|
51
|
+
grouped[resource] = [action];
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
existing.push(action);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return grouped;
|
|
58
|
+
})();
|
|
59
|
+
/**
|
|
60
|
+
* Returns every registered `resource:action` key. Used by the
|
|
61
|
+
* metadata↔dispatch consistency tests to catch handlers added without a
|
|
62
|
+
* matching metadata entry — the inverse of the metadata-first check.
|
|
63
|
+
*/
|
|
64
|
+
export function getRegisteredActions() {
|
|
65
|
+
return Object.keys(HANDLERS);
|
|
66
|
+
}
|
|
67
|
+
export function dispatch(resource, action) {
|
|
68
|
+
const actions = RESOURCES[resource];
|
|
69
|
+
if (actions === undefined) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: `Unknown resource '${resource}'. Use: ${Object.keys(RESOURCES).join(', ')}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (!actions.includes(action)) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: `Unknown action '${action}' for ${resource}. Use: ${actions.join(', ')}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const handler = HANDLERS[`${resource}:${action}`];
|
|
82
|
+
if (handler === undefined) {
|
|
83
|
+
return { ok: false, error: `No handler registered for ${resource}:${action}` };
|
|
84
|
+
}
|
|
85
|
+
return { ok: true, handler };
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=dispatch.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { TestRailClient } from '../client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parsed CLI argument bundle passed to every handler.
|
|
4
|
+
*
|
|
5
|
+
* `pathParams` is the slice of positional args after `[resource, action]` —
|
|
6
|
+
* read handlers consume `pathParams[0]` (the single id), write handlers may
|
|
7
|
+
* consume multiple (e.g., `result add <run_id> <case_id>` uses [0] and [1]).
|
|
8
|
+
*/
|
|
9
|
+
export interface HandlerArgs {
|
|
10
|
+
pathParams: readonly string[];
|
|
11
|
+
projectId?: string;
|
|
12
|
+
suiteId?: string;
|
|
13
|
+
runId?: string;
|
|
14
|
+
caseId?: string;
|
|
15
|
+
limit?: string;
|
|
16
|
+
offset?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Raw inputs for the body-source resolver. The handler decides whether to
|
|
20
|
+
* consume any of these (write handlers do; read handlers ignore). When all
|
|
21
|
+
* three are absent for a write action, the resolver emits a "body required"
|
|
22
|
+
* error.
|
|
23
|
+
*
|
|
24
|
+
* `readStdin` is a thunk rather than pre-read contents so stdin is *only*
|
|
25
|
+
* drained when the resolver actually selects it as the body source. Read
|
|
26
|
+
* actions and no-body writes (`run close`) never invoke it — avoiding the
|
|
27
|
+
* "tail -f | testrail run close" hang and the cost of slurping a large
|
|
28
|
+
* redirected stdin that the handler will throw away.
|
|
29
|
+
*/
|
|
30
|
+
export interface BodyInput {
|
|
31
|
+
dataFlag?: string;
|
|
32
|
+
dataFileFlag?: string;
|
|
33
|
+
readStdin?: () => string;
|
|
34
|
+
}
|
|
35
|
+
export interface HandlerContext {
|
|
36
|
+
client: TestRailClient;
|
|
37
|
+
args: HandlerArgs;
|
|
38
|
+
bodyInput: BodyInput;
|
|
39
|
+
dryRun: boolean;
|
|
40
|
+
out: (data: unknown) => void;
|
|
41
|
+
}
|
|
42
|
+
export type Handler = (ctx: HandlerContext) => Promise<void>;
|
|
43
|
+
//# sourceMappingURL=handler-context.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { parseId } from '../ids.js';
|
|
2
|
+
import { resolveBody } from '../body.js';
|
|
3
|
+
import { AddCasePayloadSchema, UpdateCasePayloadSchema } from '../../schemas.js';
|
|
4
|
+
export async function handleCaseAdd(ctx) {
|
|
5
|
+
const sectionId = parseId(ctx.args.pathParams[0], 'section_id');
|
|
6
|
+
const body = resolveBody(ctx.bodyInput, AddCasePayloadSchema);
|
|
7
|
+
if (!body.ok)
|
|
8
|
+
throw new Error(body.error);
|
|
9
|
+
if (ctx.dryRun) {
|
|
10
|
+
ctx.out({ dryRun: true, action: 'case add', sectionId, payload: body.payload, source: body.source });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
ctx.out(await ctx.client.addCase(sectionId, body.payload));
|
|
14
|
+
}
|
|
15
|
+
export async function handleCaseUpdate(ctx) {
|
|
16
|
+
const caseId = parseId(ctx.args.pathParams[0], 'case_id');
|
|
17
|
+
const body = resolveBody(ctx.bodyInput, UpdateCasePayloadSchema);
|
|
18
|
+
if (!body.ok)
|
|
19
|
+
throw new Error(body.error);
|
|
20
|
+
if (ctx.dryRun) {
|
|
21
|
+
ctx.out({ dryRun: true, action: 'case update', caseId, payload: body.payload, source: body.source });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
ctx.out(await ctx.client.updateCase(caseId, body.payload));
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=case-write.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { parseId, optInt } from '../ids.js';
|
|
2
|
+
export async function handleCaseGet(ctx) {
|
|
3
|
+
const id = parseId(ctx.args.pathParams[0], 'case id');
|
|
4
|
+
ctx.out(await ctx.client.getCase(id));
|
|
5
|
+
}
|
|
6
|
+
export async function handleCaseList(ctx) {
|
|
7
|
+
const pid = parseId(ctx.args.projectId, '--project-id');
|
|
8
|
+
const suiteId = optInt(ctx.args.suiteId);
|
|
9
|
+
ctx.out(await ctx.client.getCases(pid, suiteId !== undefined ? { suiteId } : undefined));
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=case.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { parseId, optInt } from '../ids.js';
|
|
2
|
+
export async function handleMilestoneGet(ctx) {
|
|
3
|
+
const id = parseId(ctx.args.pathParams[0], 'milestone id');
|
|
4
|
+
ctx.out(await ctx.client.getMilestone(id));
|
|
5
|
+
}
|
|
6
|
+
export async function handleMilestoneList(ctx) {
|
|
7
|
+
const pid = parseId(ctx.args.projectId, '--project-id');
|
|
8
|
+
const limit = optInt(ctx.args.limit);
|
|
9
|
+
const offset = optInt(ctx.args.offset);
|
|
10
|
+
ctx.out(await ctx.client.getMilestones(pid, {
|
|
11
|
+
...(limit !== undefined && { limit }),
|
|
12
|
+
...(offset !== undefined && { offset }),
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=milestone.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { parseId, optInt } from '../ids.js';
|
|
2
|
+
export async function handleProjectGet(ctx) {
|
|
3
|
+
const id = parseId(ctx.args.pathParams[0], 'project id');
|
|
4
|
+
ctx.out(await ctx.client.getProject(id));
|
|
5
|
+
}
|
|
6
|
+
export async function handleProjectList(ctx) {
|
|
7
|
+
const limit = optInt(ctx.args.limit);
|
|
8
|
+
const offset = optInt(ctx.args.offset);
|
|
9
|
+
ctx.out(await ctx.client.getProjects(limit, offset));
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parseId } from '../ids.js';
|
|
2
|
+
import { resolveBody } from '../body.js';
|
|
3
|
+
import { AddResultPayloadSchema, AddResultsForCasesPayloadSchema } from '../../schemas.js';
|
|
4
|
+
export async function handleResultAdd(ctx) {
|
|
5
|
+
const runId = parseId(ctx.args.pathParams[0], 'run_id');
|
|
6
|
+
const caseId = parseId(ctx.args.pathParams[1], 'case_id');
|
|
7
|
+
const body = resolveBody(ctx.bodyInput, AddResultPayloadSchema);
|
|
8
|
+
if (!body.ok)
|
|
9
|
+
throw new Error(body.error);
|
|
10
|
+
if (ctx.dryRun) {
|
|
11
|
+
ctx.out({
|
|
12
|
+
dryRun: true,
|
|
13
|
+
action: 'result add',
|
|
14
|
+
runId,
|
|
15
|
+
caseId,
|
|
16
|
+
payload: body.payload,
|
|
17
|
+
source: body.source,
|
|
18
|
+
});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
ctx.out(await ctx.client.addResultForCase(runId, caseId, body.payload));
|
|
22
|
+
}
|
|
23
|
+
export async function handleResultAddBulk(ctx) {
|
|
24
|
+
const runId = parseId(ctx.args.pathParams[0], 'run_id');
|
|
25
|
+
const body = resolveBody(ctx.bodyInput, AddResultsForCasesPayloadSchema);
|
|
26
|
+
if (!body.ok)
|
|
27
|
+
throw new Error(body.error);
|
|
28
|
+
if (ctx.dryRun) {
|
|
29
|
+
ctx.out({
|
|
30
|
+
dryRun: true,
|
|
31
|
+
action: 'result add-bulk',
|
|
32
|
+
runId,
|
|
33
|
+
payload: body.payload,
|
|
34
|
+
source: body.source,
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
ctx.out(await ctx.client.addResultsForCases(runId, body.payload));
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=result-write.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { parseId, optInt } from '../ids.js';
|
|
2
|
+
export async function handleResultList(ctx) {
|
|
3
|
+
const rid = parseId(ctx.args.runId, '--run-id');
|
|
4
|
+
const limit = optInt(ctx.args.limit);
|
|
5
|
+
const offset = optInt(ctx.args.offset);
|
|
6
|
+
ctx.out(await ctx.client.getResultsForRun(rid, {
|
|
7
|
+
...(limit !== undefined && { limit }),
|
|
8
|
+
...(offset !== undefined && { offset }),
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=result.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HandlerContext } from '../handler-context.js';
|
|
2
|
+
export declare function handleRunAdd(ctx: HandlerContext): Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* Close a run. Unlike the other write actions this takes no body — just a
|
|
5
|
+
* single `run_id` path param. POST has no payload, so the body-source
|
|
6
|
+
* resolver is not consulted; any `--data` / `--data-file` / stdin supplied
|
|
7
|
+
* for this action is silently ignored.
|
|
8
|
+
*/
|
|
9
|
+
export declare function handleRunClose(ctx: HandlerContext): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=run-write.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parseId } from '../ids.js';
|
|
2
|
+
import { resolveBody } from '../body.js';
|
|
3
|
+
import { AddRunPayloadSchema } from '../../schemas.js';
|
|
4
|
+
export async function handleRunAdd(ctx) {
|
|
5
|
+
const projectId = parseId(ctx.args.pathParams[0], 'project_id');
|
|
6
|
+
const body = resolveBody(ctx.bodyInput, AddRunPayloadSchema);
|
|
7
|
+
if (!body.ok)
|
|
8
|
+
throw new Error(body.error);
|
|
9
|
+
if (ctx.dryRun) {
|
|
10
|
+
ctx.out({ dryRun: true, action: 'run add', projectId, payload: body.payload, source: body.source });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
ctx.out(await ctx.client.addRun(projectId, body.payload));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Close a run. Unlike the other write actions this takes no body — just a
|
|
17
|
+
* single `run_id` path param. POST has no payload, so the body-source
|
|
18
|
+
* resolver is not consulted; any `--data` / `--data-file` / stdin supplied
|
|
19
|
+
* for this action is silently ignored.
|
|
20
|
+
*/
|
|
21
|
+
export async function handleRunClose(ctx) {
|
|
22
|
+
const runId = parseId(ctx.args.pathParams[0], 'run_id');
|
|
23
|
+
if (ctx.dryRun) {
|
|
24
|
+
ctx.out({ dryRun: true, action: 'run close', runId });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
ctx.out(await ctx.client.closeRun(runId));
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=run-write.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { parseId, optInt } from '../ids.js';
|
|
2
|
+
export async function handleRunGet(ctx) {
|
|
3
|
+
const id = parseId(ctx.args.pathParams[0], 'run id');
|
|
4
|
+
ctx.out(await ctx.client.getRun(id));
|
|
5
|
+
}
|
|
6
|
+
export async function handleRunList(ctx) {
|
|
7
|
+
const pid = parseId(ctx.args.projectId, '--project-id');
|
|
8
|
+
const limit = optInt(ctx.args.limit);
|
|
9
|
+
const offset = optInt(ctx.args.offset);
|
|
10
|
+
ctx.out(await ctx.client.getRuns(pid, {
|
|
11
|
+
...(limit !== undefined && { limit }),
|
|
12
|
+
...(offset !== undefined && { offset }),
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parseId } from '../ids.js';
|
|
2
|
+
export async function handleSuiteGet(ctx) {
|
|
3
|
+
const id = parseId(ctx.args.pathParams[0], 'suite id');
|
|
4
|
+
ctx.out(await ctx.client.getSuite(id));
|
|
5
|
+
}
|
|
6
|
+
export async function handleSuiteList(ctx) {
|
|
7
|
+
const pid = parseId(ctx.args.projectId, '--project-id');
|
|
8
|
+
ctx.out(await ctx.client.getSuites(pid));
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=suite.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { parseId, optInt } from '../ids.js';
|
|
2
|
+
export async function handleUserGet(ctx) {
|
|
3
|
+
const id = parseId(ctx.args.pathParams[0], 'user id');
|
|
4
|
+
ctx.out(await ctx.client.getUser(id));
|
|
5
|
+
}
|
|
6
|
+
export async function handleUserList(ctx) {
|
|
7
|
+
const limit = optInt(ctx.args.limit);
|
|
8
|
+
const offset = optInt(ctx.args.offset);
|
|
9
|
+
ctx.out(await ctx.client.getUsers(limit, offset));
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=user.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare class IdParseError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare function parseId(raw: string | undefined, name: string): number;
|
|
5
|
+
export declare function optInt(raw: string | undefined): number | undefined;
|
|
6
|
+
//# sourceMappingURL=ids.d.ts.map
|
package/dist/cli/ids.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class IdParseError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'IdParseError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function parseId(raw, name) {
|
|
8
|
+
const n = Number(raw);
|
|
9
|
+
if (raw === undefined || raw === '' || !Number.isInteger(n) || n <= 0) {
|
|
10
|
+
throw new IdParseError(`${name} must be a positive integer (got: ${raw ?? '(none)'})`);
|
|
11
|
+
}
|
|
12
|
+
return n;
|
|
13
|
+
}
|
|
14
|
+
export function optInt(raw) {
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
const n = Number(raw);
|
|
18
|
+
return Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=ids.js.map
|