@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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
AddCasePayloadSchema,
|
|
4
|
+
UpdateCasePayloadSchema,
|
|
5
|
+
AddRunPayloadSchema,
|
|
6
|
+
AddResultPayloadSchema,
|
|
7
|
+
AddResultsForCasesPayloadSchema,
|
|
8
|
+
} from '../schemas.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Declarative spec for every resource:action exposed by the CLI.
|
|
12
|
+
*
|
|
13
|
+
* Single source of truth shared by:
|
|
14
|
+
* - PR 3 tests: assert both directions of the metadata↔dispatch
|
|
15
|
+
* correspondence — every `ACTIONS` entry must have a registered handler
|
|
16
|
+
* in `dispatch.ts` HANDLERS, and every HANDLERS key must have an
|
|
17
|
+
* `ACTIONS` entry. Catches drift in either direction.
|
|
18
|
+
* - PR 4 skill generator: renders the `<!-- GENERATED:command-table -->` and
|
|
19
|
+
* `<!-- GENERATED:payload-schemas -->` regions of `skill/SKILL.md` from
|
|
20
|
+
* this array.
|
|
21
|
+
*
|
|
22
|
+
* Adding a new action requires touching exactly two places: the handler in
|
|
23
|
+
* `src/cli/handlers/`, and an entry here. The dispatcher and the skill stay
|
|
24
|
+
* accurate automatically.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface PathParam {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ActionSpec {
|
|
33
|
+
resource: string;
|
|
34
|
+
action: string;
|
|
35
|
+
summary: string;
|
|
36
|
+
pathParams: readonly PathParam[];
|
|
37
|
+
/** Zod schema for the request body. `undefined` for read actions and for
|
|
38
|
+
* no-body POSTs like `run close`. */
|
|
39
|
+
bodySchema?: z.ZodTypeAny;
|
|
40
|
+
/** True for write actions (POST / payload-bearing). Affects skill recipes,
|
|
41
|
+
* generator output, and `--dry-run` applicability. */
|
|
42
|
+
isWrite: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const ACTIONS: readonly ActionSpec[] = [
|
|
46
|
+
// ── Read actions ──────────────────────────────────────────────────────
|
|
47
|
+
{
|
|
48
|
+
resource: 'project',
|
|
49
|
+
action: 'get',
|
|
50
|
+
summary: 'Fetch a single project by ID',
|
|
51
|
+
pathParams: [{ name: 'project_id', description: 'TestRail project ID' }],
|
|
52
|
+
isWrite: false,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
resource: 'project',
|
|
56
|
+
action: 'list',
|
|
57
|
+
summary: 'List all projects (paginated)',
|
|
58
|
+
pathParams: [],
|
|
59
|
+
isWrite: false,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
resource: 'suite',
|
|
63
|
+
action: 'get',
|
|
64
|
+
summary: 'Fetch a single suite by ID',
|
|
65
|
+
pathParams: [{ name: 'suite_id', description: 'TestRail suite ID' }],
|
|
66
|
+
isWrite: false,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
resource: 'suite',
|
|
70
|
+
action: 'list',
|
|
71
|
+
summary: 'List suites in a project',
|
|
72
|
+
pathParams: [],
|
|
73
|
+
isWrite: false,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
resource: 'case',
|
|
77
|
+
action: 'get',
|
|
78
|
+
summary: 'Fetch a single test case by ID',
|
|
79
|
+
pathParams: [{ name: 'case_id', description: 'TestRail case ID' }],
|
|
80
|
+
isWrite: false,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
resource: 'case',
|
|
84
|
+
action: 'list',
|
|
85
|
+
summary: 'List cases in a project (optionally filtered by suite)',
|
|
86
|
+
pathParams: [],
|
|
87
|
+
isWrite: false,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
resource: 'run',
|
|
91
|
+
action: 'get',
|
|
92
|
+
summary: 'Fetch a single run by ID',
|
|
93
|
+
pathParams: [{ name: 'run_id', description: 'TestRail run ID' }],
|
|
94
|
+
isWrite: false,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
resource: 'run',
|
|
98
|
+
action: 'list',
|
|
99
|
+
summary: 'List runs in a project (paginated)',
|
|
100
|
+
pathParams: [],
|
|
101
|
+
isWrite: false,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
resource: 'result',
|
|
105
|
+
action: 'list',
|
|
106
|
+
summary: 'List results for a run (paginated)',
|
|
107
|
+
pathParams: [],
|
|
108
|
+
isWrite: false,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
resource: 'milestone',
|
|
112
|
+
action: 'get',
|
|
113
|
+
summary: 'Fetch a single milestone by ID',
|
|
114
|
+
pathParams: [{ name: 'milestone_id', description: 'TestRail milestone ID' }],
|
|
115
|
+
isWrite: false,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
resource: 'milestone',
|
|
119
|
+
action: 'list',
|
|
120
|
+
summary: 'List milestones in a project (paginated)',
|
|
121
|
+
pathParams: [],
|
|
122
|
+
isWrite: false,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
resource: 'user',
|
|
126
|
+
action: 'get',
|
|
127
|
+
summary: 'Fetch a single user by ID',
|
|
128
|
+
pathParams: [{ name: 'user_id', description: 'TestRail user ID' }],
|
|
129
|
+
isWrite: false,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
resource: 'user',
|
|
133
|
+
action: 'list',
|
|
134
|
+
summary: 'List users (paginated)',
|
|
135
|
+
pathParams: [],
|
|
136
|
+
isWrite: false,
|
|
137
|
+
},
|
|
138
|
+
// ── Write actions ─────────────────────────────────────────────────────
|
|
139
|
+
{
|
|
140
|
+
resource: 'case',
|
|
141
|
+
action: 'add',
|
|
142
|
+
summary: 'Create a new test case under a section',
|
|
143
|
+
pathParams: [{ name: 'section_id', description: 'Section to create the case under' }],
|
|
144
|
+
bodySchema: AddCasePayloadSchema,
|
|
145
|
+
isWrite: true,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
resource: 'case',
|
|
149
|
+
action: 'update',
|
|
150
|
+
summary: 'Update an existing test case (partial fields)',
|
|
151
|
+
pathParams: [{ name: 'case_id', description: 'TestRail case ID' }],
|
|
152
|
+
bodySchema: UpdateCasePayloadSchema,
|
|
153
|
+
isWrite: true,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
resource: 'run',
|
|
157
|
+
action: 'add',
|
|
158
|
+
summary: 'Create a new test run in a project',
|
|
159
|
+
pathParams: [{ name: 'project_id', description: 'TestRail project ID' }],
|
|
160
|
+
bodySchema: AddRunPayloadSchema,
|
|
161
|
+
isWrite: true,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
resource: 'run',
|
|
165
|
+
action: 'close',
|
|
166
|
+
summary: 'Close a test run (no body)',
|
|
167
|
+
pathParams: [{ name: 'run_id', description: 'TestRail run ID' }],
|
|
168
|
+
isWrite: true,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
resource: 'result',
|
|
172
|
+
action: 'add',
|
|
173
|
+
summary: 'Record a single result for a case in a run',
|
|
174
|
+
pathParams: [
|
|
175
|
+
{ name: 'run_id', description: 'TestRail run ID' },
|
|
176
|
+
{ name: 'case_id', description: 'TestRail case ID' },
|
|
177
|
+
],
|
|
178
|
+
bodySchema: AddResultPayloadSchema,
|
|
179
|
+
isWrite: true,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
resource: 'result',
|
|
183
|
+
action: 'add-bulk',
|
|
184
|
+
summary: 'Record multiple results for cases in one API call',
|
|
185
|
+
pathParams: [{ name: 'run_id', description: 'TestRail run ID' }],
|
|
186
|
+
bodySchema: AddResultsForCasesPayloadSchema,
|
|
187
|
+
isWrite: true,
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
/** Look up the spec for a resource:action pair, or return undefined. */
|
|
192
|
+
export function getActionSpec(resource: string, action: string): ActionSpec | undefined {
|
|
193
|
+
return ACTIONS.find((a) => a.resource === resource && a.action === action);
|
|
194
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export interface OutputOptions {
|
|
2
|
+
quiet: boolean;
|
|
3
|
+
format: 'json' | 'table';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface Output {
|
|
7
|
+
out: (data: unknown) => void;
|
|
8
|
+
err: (message: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function valueToString(v: unknown): string {
|
|
12
|
+
if (v === null || v === undefined) return '';
|
|
13
|
+
if (typeof v === 'object') {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.stringify(v);
|
|
16
|
+
} catch {
|
|
17
|
+
// JSON.stringify throws on circular refs and nested BigInt.
|
|
18
|
+
return '[Object]';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (typeof v === 'string') return v;
|
|
22
|
+
if (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'bigint') return String(v);
|
|
23
|
+
if (typeof v === 'symbol') return v.toString();
|
|
24
|
+
return '[Function]';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getField(row: unknown, key: string): unknown {
|
|
28
|
+
if (typeof row !== 'object' || row === null) return undefined;
|
|
29
|
+
return (row as Record<string, unknown>)[key];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function renderTable(data: unknown): string {
|
|
33
|
+
const rows: unknown[] = Array.isArray(data) ? (data as unknown[]) : [data];
|
|
34
|
+
if (rows.length === 0) return '(empty)';
|
|
35
|
+
|
|
36
|
+
const first: unknown = rows[0];
|
|
37
|
+
if (typeof first !== 'object' || first === null) {
|
|
38
|
+
return rows.map(String).join('\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const keys = Object.keys(first);
|
|
42
|
+
const widths = keys.map((k) => Math.max(k.length, ...rows.map((r) => valueToString(getField(r, k)).length)));
|
|
43
|
+
|
|
44
|
+
const line = widths.map((w) => '-'.repeat(w)).join('-+-');
|
|
45
|
+
const header = keys.map((k, i) => k.padEnd(widths[i] ?? k.length)).join(' | ');
|
|
46
|
+
const body = rows.map((r) =>
|
|
47
|
+
keys.map((k, i) => valueToString(getField(r, k)).padEnd(widths[i] ?? k.length)).join(' | '),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return [header, line, ...body].join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Best-effort JSON.stringify with two fallbacks, guaranteeing the return
|
|
55
|
+
* value is always parseable JSON for downstream tools (e.g., `jq`):
|
|
56
|
+
*
|
|
57
|
+
* 1. If serialization throws (circular reference, nested BigInt, etc.),
|
|
58
|
+
* emit a structured `{ error, message }` JSON object.
|
|
59
|
+
* 2. If `JSON.stringify` returns the JS value `undefined` — which it does
|
|
60
|
+
* for `undefined`, function, or symbol inputs — emit the JSON literal
|
|
61
|
+
* `"null"`. Without this guard, the caller's template literal would
|
|
62
|
+
* coerce that `undefined` to the string `"undefined"`, which is not
|
|
63
|
+
* valid JSON.
|
|
64
|
+
*
|
|
65
|
+
* Exported so unit tests can verify the fallbacks without spawning a
|
|
66
|
+
* subprocess.
|
|
67
|
+
*/
|
|
68
|
+
export function safeJsonStringify(data: unknown): string {
|
|
69
|
+
try {
|
|
70
|
+
// JSON.stringify returns the JS value undefined for inputs without a
|
|
71
|
+
// JSON representation (undefined, function, symbol); fall back to the
|
|
72
|
+
// JSON literal "null" so the result is always a parseable string.
|
|
73
|
+
return JSON.stringify(data, null, 2) ?? 'null';
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return JSON.stringify(
|
|
76
|
+
{ error: 'unserializable', message: e instanceof Error ? e.message : String(e) },
|
|
77
|
+
null,
|
|
78
|
+
2,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createOutput(opts: OutputOptions): Output {
|
|
84
|
+
const out = (data: unknown): void => {
|
|
85
|
+
if (opts.quiet) return;
|
|
86
|
+
if (opts.format === 'table') {
|
|
87
|
+
process.stdout.write(`${renderTable(data)}\n`);
|
|
88
|
+
} else {
|
|
89
|
+
process.stdout.write(`${safeJsonStringify(data)}\n`);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const err = (message: string): void => {
|
|
93
|
+
if (!opts.quiet) process.stderr.write(`Error: ${message}\n`);
|
|
94
|
+
};
|
|
95
|
+
return { out, err };
|
|
96
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,287 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import { createRequire } from 'node:module';
|
|
4
|
-
import { TestRailClient } from './client.js';
|
|
5
|
-
import type { TestRailConfig } from './types.js';
|
|
6
|
-
|
|
7
|
-
// ── Version ───────────────────────────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
const require = createRequire(import.meta.url);
|
|
10
|
-
const VERSION: string = (require('../package.json') as { version: string }).version;
|
|
11
|
-
|
|
12
|
-
// ── Arg Parsing ───────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
const { values, positionals } = parseArgs({
|
|
15
|
-
args: process.argv.slice(2),
|
|
16
|
-
options: {
|
|
17
|
-
'base-url': { type: 'string' },
|
|
18
|
-
email: { type: 'string' },
|
|
19
|
-
'api-key': { type: 'string' },
|
|
20
|
-
format: { type: 'string', default: 'json' },
|
|
21
|
-
quiet: { type: 'boolean', default: false },
|
|
22
|
-
help: { type: 'boolean', default: false },
|
|
23
|
-
version: { type: 'boolean', default: false },
|
|
24
|
-
'project-id': { type: 'string' },
|
|
25
|
-
'suite-id': { type: 'string' },
|
|
26
|
-
'run-id': { type: 'string' },
|
|
27
|
-
'case-id': { type: 'string' },
|
|
28
|
-
limit: { type: 'string' },
|
|
29
|
-
offset: { type: 'string' },
|
|
30
|
-
},
|
|
31
|
-
allowPositionals: true,
|
|
32
|
-
strict: false,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// ── Output Helpers ────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
const quiet = values.quiet === true;
|
|
38
|
-
const format = values.format ?? 'json';
|
|
39
|
-
|
|
40
|
-
function out(data: unknown): void {
|
|
41
|
-
if (quiet) return;
|
|
42
|
-
if (format === 'table') {
|
|
43
|
-
process.stdout.write(`${renderTable(data)}\n`);
|
|
44
|
-
} else {
|
|
45
|
-
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function err(message: string): void {
|
|
50
|
-
if (!quiet) process.stderr.write(`Error: ${message}\n`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function valueToString(v: unknown): string {
|
|
54
|
-
if (v === null || v === undefined) return '';
|
|
55
|
-
if (typeof v === 'object') return JSON.stringify(v);
|
|
56
|
-
if (typeof v === 'string') return v;
|
|
57
|
-
if (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'bigint') return String(v);
|
|
58
|
-
if (typeof v === 'symbol') return v.toString();
|
|
59
|
-
return '[Function]';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function renderTable(data: unknown): string {
|
|
63
|
-
const rows: unknown[] = Array.isArray(data) ? (data as unknown[]) : [data];
|
|
64
|
-
if (rows.length === 0) return '(empty)';
|
|
65
|
-
|
|
66
|
-
const first: unknown = rows[0];
|
|
67
|
-
if (typeof first !== 'object' || first === null) {
|
|
68
|
-
return rows.map(String).join('\n');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const keys = Object.keys(first);
|
|
72
|
-
const widths = keys.map((k) =>
|
|
73
|
-
Math.max(k.length, ...rows.map((r) => valueToString((r as Record<string, unknown>)[k]).length)),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const line = widths.map((w) => '-'.repeat(w)).join('-+-');
|
|
77
|
-
const header = keys.map((k, i) => k.padEnd(widths[i] ?? k.length)).join(' | ');
|
|
78
|
-
const body = rows.map((r) =>
|
|
79
|
-
keys.map((k, i) => valueToString((r as Record<string, unknown>)[k]).padEnd(widths[i] ?? k.length)).join(' | '),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
return [header, line, ...body].join('\n');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ── Help ──────────────────────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
const HELP = `
|
|
88
|
-
testrail <resource> <action> [id] [options]
|
|
89
|
-
|
|
90
|
-
Resources & actions:
|
|
91
|
-
project get <id> | list [--limit N] [--offset N]
|
|
92
|
-
suite get <id> | list --project-id <id>
|
|
93
|
-
case get <id> | list --project-id <id> [--suite-id <id>]
|
|
94
|
-
run get <id> | list --project-id <id> [--limit N] [--offset N]
|
|
95
|
-
result list --run-id <id> [--limit N] [--offset N]
|
|
96
|
-
milestone get <id> | list --project-id <id> [--limit N] [--offset N]
|
|
97
|
-
user get <id> | list [--limit N] [--offset N]
|
|
98
|
-
|
|
99
|
-
Auth (env var or flag):
|
|
100
|
-
TESTRAIL_BASE_URL / --base-url <url>
|
|
101
|
-
TESTRAIL_EMAIL / --email <email>
|
|
102
|
-
TESTRAIL_API_KEY / --api-key <key>
|
|
103
|
-
|
|
104
|
-
Options:
|
|
105
|
-
--format json|table Output format (default: json)
|
|
106
|
-
--quiet Suppress output; use exit code 0/1
|
|
107
|
-
--help Show this help
|
|
108
|
-
--version Print version
|
|
109
|
-
`.trim();
|
|
110
|
-
|
|
111
|
-
// ── Entry Point ───────────────────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
if (values.version === true) {
|
|
114
|
-
process.stdout.write(`testrail-cli v${VERSION}\n`);
|
|
115
|
-
process.exit(0);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (values.help === true || positionals.length === 0) {
|
|
119
|
-
process.stdout.write(`${HELP}\n`);
|
|
120
|
-
process.exit(0);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const [resource, action, idArg] = positionals;
|
|
124
|
-
|
|
125
|
-
if (resource === undefined || resource === '' || action === undefined || action === '') {
|
|
126
|
-
process.stderr.write('Usage: testrail <resource> <action> [id] [options]\nRun with --help for details.\n');
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ── Auth Resolution ───────────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
const baseUrl = (values['base-url'] as string | undefined) ?? process.env['TESTRAIL_BASE_URL'];
|
|
133
|
-
const email = (values['email'] as string | undefined) ?? process.env['TESTRAIL_EMAIL'];
|
|
134
|
-
const apiKey = (values['api-key'] as string | undefined) ?? process.env['TESTRAIL_API_KEY'];
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
baseUrl === undefined ||
|
|
138
|
-
baseUrl === '' ||
|
|
139
|
-
email === undefined ||
|
|
140
|
-
email === '' ||
|
|
141
|
-
apiKey === undefined ||
|
|
142
|
-
apiKey === ''
|
|
143
|
-
) {
|
|
144
|
-
err(
|
|
145
|
-
'Missing auth. Set TESTRAIL_BASE_URL, TESTRAIL_EMAIL, TESTRAIL_API_KEY or use --base-url, --email, --api-key flags.',
|
|
146
|
-
);
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const config: TestRailConfig = { baseUrl, email, apiKey };
|
|
151
|
-
const client = new TestRailClient(config);
|
|
152
|
-
|
|
153
|
-
// ── Numeric Helpers ───────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
function parseId(raw: string | undefined, name: string): number {
|
|
156
|
-
const n = Number(raw);
|
|
157
|
-
if (raw === undefined || raw === '' || !Number.isInteger(n) || n <= 0) {
|
|
158
|
-
err(`${name} must be a positive integer (got: ${raw ?? '(none)'})`);
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
161
|
-
return n;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function optInt(raw: string | undefined): number | undefined {
|
|
165
|
-
if (raw === undefined) return undefined;
|
|
166
|
-
const n = Number(raw);
|
|
167
|
-
return Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const suiteId = optInt(values['suite-id'] as string | undefined);
|
|
171
|
-
const limit = optInt(values.limit as string | undefined);
|
|
172
|
-
const offset = optInt(values.offset as string | undefined);
|
|
173
|
-
|
|
174
|
-
// ── Command Dispatch ──────────────────────────────────────────────────────────
|
|
175
|
-
|
|
176
|
-
async function run(res: string, act: string): Promise<void> {
|
|
177
|
-
switch (res) {
|
|
178
|
-
case 'project': {
|
|
179
|
-
if (act === 'get') {
|
|
180
|
-
const id = parseId(idArg, 'project id');
|
|
181
|
-
out(await client.getProject(id));
|
|
182
|
-
} else if (act === 'list') {
|
|
183
|
-
out(await client.getProjects(limit, offset));
|
|
184
|
-
} else {
|
|
185
|
-
err(`Unknown action '${act}' for project. Use: get, list`);
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
case 'suite': {
|
|
191
|
-
if (act === 'get') {
|
|
192
|
-
const id = parseId(idArg, 'suite id');
|
|
193
|
-
out(await client.getSuite(id));
|
|
194
|
-
} else if (act === 'list') {
|
|
195
|
-
const pid = parseId(values['project-id'] as string | undefined, '--project-id');
|
|
196
|
-
out(await client.getSuites(pid));
|
|
197
|
-
} else {
|
|
198
|
-
err(`Unknown action '${act}' for suite. Use: get, list`);
|
|
199
|
-
process.exit(1);
|
|
200
|
-
}
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
case 'case': {
|
|
204
|
-
if (act === 'get') {
|
|
205
|
-
const id = parseId(idArg, 'case id');
|
|
206
|
-
out(await client.getCase(id));
|
|
207
|
-
} else if (act === 'list') {
|
|
208
|
-
const pid = parseId(values['project-id'] as string | undefined, '--project-id');
|
|
209
|
-
out(await client.getCases(pid, suiteId !== undefined ? { suiteId } : undefined));
|
|
210
|
-
} else {
|
|
211
|
-
err(`Unknown action '${act}' for case. Use: get, list`);
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
case 'run': {
|
|
217
|
-
if (act === 'get') {
|
|
218
|
-
const id = parseId(idArg, 'run id');
|
|
219
|
-
out(await client.getRun(id));
|
|
220
|
-
} else if (act === 'list') {
|
|
221
|
-
const pid = parseId(values['project-id'] as string | undefined, '--project-id');
|
|
222
|
-
out(
|
|
223
|
-
await client.getRuns(pid, {
|
|
224
|
-
...(limit !== undefined && { limit }),
|
|
225
|
-
...(offset !== undefined && { offset }),
|
|
226
|
-
}),
|
|
227
|
-
);
|
|
228
|
-
} else {
|
|
229
|
-
err(`Unknown action '${act}' for run. Use: get, list`);
|
|
230
|
-
process.exit(1);
|
|
231
|
-
}
|
|
232
|
-
break;
|
|
233
|
-
}
|
|
234
|
-
case 'result': {
|
|
235
|
-
if (act === 'list') {
|
|
236
|
-
const rid = parseId(values['run-id'] as string | undefined, '--run-id');
|
|
237
|
-
const resultOpts = { ...(limit !== undefined && { limit }), ...(offset !== undefined && { offset }) };
|
|
238
|
-
out(await client.getResultsForRun(rid, resultOpts));
|
|
239
|
-
} else {
|
|
240
|
-
err(`Unknown action '${act}' for result. Use: list`);
|
|
241
|
-
process.exit(1);
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
case 'milestone': {
|
|
246
|
-
if (act === 'get') {
|
|
247
|
-
const id = parseId(idArg, 'milestone id');
|
|
248
|
-
out(await client.getMilestone(id));
|
|
249
|
-
} else if (act === 'list') {
|
|
250
|
-
const pid = parseId(values['project-id'] as string | undefined, '--project-id');
|
|
251
|
-
const msOpts = { ...(limit !== undefined && { limit }), ...(offset !== undefined && { offset }) };
|
|
252
|
-
out(await client.getMilestones(pid, msOpts));
|
|
253
|
-
} else {
|
|
254
|
-
err(`Unknown action '${act}' for milestone. Use: get, list`);
|
|
255
|
-
process.exit(1);
|
|
256
|
-
}
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
case 'user': {
|
|
260
|
-
if (act === 'get') {
|
|
261
|
-
const id = parseId(idArg, 'user id');
|
|
262
|
-
out(await client.getUser(id));
|
|
263
|
-
} else if (act === 'list') {
|
|
264
|
-
out(await client.getUsers(limit, offset));
|
|
265
|
-
} else {
|
|
266
|
-
err(`Unknown action '${act}' for user. Use: get, list`);
|
|
267
|
-
process.exit(1);
|
|
268
|
-
}
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
default: {
|
|
272
|
-
err(`Unknown resource '${res}'. Use: project, suite, case, run, result, milestone, user`);
|
|
273
|
-
process.exit(1);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
run(resource, action)
|
|
279
|
-
.then(() => {
|
|
280
|
-
client.destroy();
|
|
281
|
-
process.exit(0);
|
|
282
|
-
})
|
|
283
|
-
.catch((e: unknown) => {
|
|
284
|
-
err(e instanceof Error ? e.message : String(e));
|
|
285
|
-
client.destroy();
|
|
286
|
-
process.exit(1);
|
|
287
|
-
});
|
|
2
|
+
import './cli/index.js';
|