@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
@@ -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 { 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
- });
2
+ import './cli/index.js';