@dichovsky/testrail-api-client 1.0.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/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,5 @@
1
+ /** Base64-encodes a string. Uses Buffer in Node.js, UTF-8-safe btoa in browsers. */
2
+ export declare function base64Encode(str: string): string;
3
+ /** Resolves after `ms` milliseconds. */
4
+ export declare function sleep(ms: number): Promise<void>;
5
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,13 @@
1
+ /** Base64-encodes a string. Uses Buffer in Node.js, UTF-8-safe btoa in browsers. */
2
+ export function base64Encode(str) {
3
+ if (typeof Buffer !== 'undefined') {
4
+ return Buffer.from(str).toString('base64');
5
+ }
6
+ // In browsers, encode the string as UTF-8 before using btoa
7
+ return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))));
8
+ }
9
+ /** Resolves after `ms` milliseconds. */
10
+ export function sleep(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@dichovsky/testrail-api-client",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript API client for TestRail",
5
+ "keywords": [
6
+ "testrail",
7
+ "api",
8
+ "client",
9
+ "typescript",
10
+ "testing"
11
+ ],
12
+ "homepage": "https://github.com/dichovsky/testrail-api-client#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/dichovsky/testrail-api-client/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/dichovsky/testrail-api-client.git"
19
+ },
20
+ "license": "MIT",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "author": "Igor Magdich",
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./cli": {
33
+ "import": "./dist/cli.js",
34
+ "default": "./dist/cli.js"
35
+ }
36
+ },
37
+ "main": "dist/index.js",
38
+ "types": "dist/index.d.ts",
39
+ "bin": {
40
+ "testrail": "./dist/cli.js"
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "src",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "scripts": {
49
+ "build": "rm -rf dist && tsc",
50
+ "clean:maps": "find dist -name '*.map' -delete",
51
+ "codemap": "node scripts/generate-codemap.js",
52
+ "format": "prettier --write .",
53
+ "format:check": "prettier --check .",
54
+ "lint": "eslint .",
55
+ "prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build && npm run clean:maps",
56
+ "test": "vitest run",
57
+ "test:coverage": "vitest run --coverage",
58
+ "test:fast": "vitest run --reporter=dot --no-coverage",
59
+ "test:watch": "vitest",
60
+ "typecheck": "tsc --noEmit"
61
+ },
62
+ "devDependencies": {
63
+ "@eslint/js": "^10.0.1",
64
+ "@types/node": "^25.6.0",
65
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
66
+ "@typescript-eslint/parser": "^8.58.1",
67
+ "@vitest/coverage-v8": "^4.1.4",
68
+ "eslint": "^10.2.0",
69
+ "prettier": "~3.8.2",
70
+ "typescript": "~6.0.2",
71
+ "vitest": "^4.1.4"
72
+ },
73
+ "engines": {
74
+ "node": "^20.19.0 || ^22.13.0 || >=24"
75
+ }
76
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
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
+ });