@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,198 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { createRequire } from 'node:module';
4
+ import { readFileSync } from 'node:fs';
5
+ import { TestRailClient } from '../client.js';
6
+ import { resolveAuth } from './auth.js';
7
+ import { createOutput } from './output.js';
8
+ import { dispatch } from './dispatch.js';
9
+ import { runInstallSkill } from './install-skill.js';
10
+ // ── Version ───────────────────────────────────────────────────────────────────
11
+ const require = createRequire(import.meta.url);
12
+ const VERSION = require('../../package.json').version;
13
+ // ── Help ──────────────────────────────────────────────────────────────────────
14
+ const HELP = `
15
+ testrail <resource> <action> [args] [options]
16
+
17
+ Read actions:
18
+ project get <id> | list [--limit N] [--offset N]
19
+ suite get <id> | list --project-id <id>
20
+ case get <id> | list --project-id <id> [--suite-id <id>]
21
+ run get <id> | list --project-id <id> [--limit N] [--offset N]
22
+ result list --run-id <id> [--limit N] [--offset N]
23
+ milestone get <id> | list --project-id <id> [--limit N] [--offset N]
24
+ user get <id> | list [--limit N] [--offset N]
25
+
26
+ Write actions (body via --data | --data-file | stdin):
27
+ case add <section_id> --data '{"title":"..."}'
28
+ case update <case_id> --data '{"title":"..."}'
29
+ run add <project_id> --data '{"name":"..."}'
30
+ run close <run_id> (no body)
31
+ result add <run_id> <case_id> --data '{"status_id":1}'
32
+ result add-bulk <run_id> --data '{"results":[{"case_id":1,"status_id":1}]}'
33
+
34
+ Meta:
35
+ install-skill [--global] [--force] [--print-path]
36
+ Install the testrail-cli skill to
37
+ ./.claude/skills/testrail-cli (default)
38
+ or ~/.claude/skills/testrail-cli (--global)
39
+
40
+ Auth (env var or flag):
41
+ TESTRAIL_BASE_URL / --base-url <url>
42
+ TESTRAIL_EMAIL / --email <email>
43
+ TESTRAIL_API_KEY / --api-key <key>
44
+
45
+ Options:
46
+ --data <json> Inline JSON body for write actions
47
+ --data-file <path> Read JSON body from file
48
+ --dry-run Validate payload but don't call the API
49
+ --format json|table Output format (default: json)
50
+ --quiet Suppress output; use exit code 0/1
51
+ --global install-skill: install to ~/.claude/skills/ (default: ./.claude/skills/)
52
+ --force install-skill: overwrite an existing SKILL.md
53
+ --print-path install-skill: print bundled SKILL.md path and exit
54
+ --help Show this help
55
+ --version Print version
56
+
57
+ For body-bearing write actions (all except 'run close'), exactly one body source
58
+ is required (--data | --data-file | stdin). Stdin is auto-detected when input
59
+ is piped (process.stdin.isTTY === false).
60
+ `.trim();
61
+ // ── Entry Point ───────────────────────────────────────────────────────────────
62
+ /**
63
+ * Compute exit code in an async function and apply `process.exit()` once
64
+ * at the very end. parseArgs and createOutput are invoked inside main() so
65
+ * any failure during initialization (e.g. an invalid CLI shape that makes
66
+ * parseArgs throw) is funneled through the same exit-code return path
67
+ * rather than escaping as an uncaught module-evaluation error.
68
+ */
69
+ async function main() {
70
+ let values;
71
+ let positionals;
72
+ try {
73
+ const parsed = parseArgs({
74
+ args: process.argv.slice(2),
75
+ options: {
76
+ 'base-url': { type: 'string' },
77
+ email: { type: 'string' },
78
+ 'api-key': { type: 'string' },
79
+ format: { type: 'string', default: 'json' },
80
+ quiet: { type: 'boolean', default: false },
81
+ help: { type: 'boolean', default: false },
82
+ version: { type: 'boolean', default: false },
83
+ 'project-id': { type: 'string' },
84
+ 'suite-id': { type: 'string' },
85
+ 'run-id': { type: 'string' },
86
+ 'case-id': { type: 'string' },
87
+ limit: { type: 'string' },
88
+ offset: { type: 'string' },
89
+ data: { type: 'string' },
90
+ 'data-file': { type: 'string' },
91
+ 'dry-run': { type: 'boolean', default: false },
92
+ global: { type: 'boolean', default: false },
93
+ force: { type: 'boolean', default: false },
94
+ 'print-path': { type: 'boolean', default: false },
95
+ },
96
+ allowPositionals: true,
97
+ strict: false,
98
+ });
99
+ values = parsed.values;
100
+ positionals = parsed.positionals;
101
+ /* v8 ignore start -- defensive: parseArgs with strict:false is highly
102
+ tolerant; this catch funnels any future-Node-version edge cases
103
+ through the controlled exit path rather than crashing the module. */
104
+ }
105
+ catch (e) {
106
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : String(e)}\n`);
107
+ return 1;
108
+ }
109
+ /* v8 ignore stop */
110
+ const quiet = values['quiet'] === true;
111
+ const formatRaw = values['format'];
112
+ const format = formatRaw === 'table' ? 'table' : 'json';
113
+ const { out, err } = createOutput({ quiet, format });
114
+ if (values['version'] === true) {
115
+ process.stdout.write(`testrail-cli v${VERSION}\n`);
116
+ return 0;
117
+ }
118
+ if (values['help'] === true || positionals.length === 0) {
119
+ process.stdout.write(`${HELP}\n`);
120
+ return 0;
121
+ }
122
+ // `install-skill` is a meta-command (manages the bundled skill on the
123
+ // user's filesystem). It deliberately sits outside the normal
124
+ // resource:action dispatch since there is no API call involved.
125
+ if (positionals[0] === 'install-skill') {
126
+ return runInstallSkill({
127
+ global: values['global'] === true,
128
+ force: values['force'] === true,
129
+ printPath: values['print-path'] === true,
130
+ quiet,
131
+ }, import.meta.url);
132
+ }
133
+ const [resource, action, ...rest] = positionals;
134
+ const pathParams = rest;
135
+ if (resource === undefined || resource === '' || action === undefined || action === '') {
136
+ process.stderr.write('Usage: testrail <resource> <action> [args] [options]\nRun with --help for details.\n');
137
+ return 1;
138
+ }
139
+ const dispatched = dispatch(resource, action);
140
+ if (!dispatched.ok) {
141
+ err(dispatched.error);
142
+ return 1;
143
+ }
144
+ const auth = resolveAuth({
145
+ baseUrl: values['base-url'],
146
+ email: values['email'],
147
+ apiKey: values['api-key'],
148
+ }, {
149
+ ...(process.env['TESTRAIL_BASE_URL'] !== undefined && {
150
+ TESTRAIL_BASE_URL: process.env['TESTRAIL_BASE_URL'],
151
+ }),
152
+ ...(process.env['TESTRAIL_EMAIL'] !== undefined && { TESTRAIL_EMAIL: process.env['TESTRAIL_EMAIL'] }),
153
+ ...(process.env['TESTRAIL_API_KEY'] !== undefined && { TESTRAIL_API_KEY: process.env['TESTRAIL_API_KEY'] }),
154
+ });
155
+ if (!auth.ok) {
156
+ err(auth.error);
157
+ return 1;
158
+ }
159
+ const args = {
160
+ pathParams,
161
+ ...(values['project-id'] !== undefined && { projectId: values['project-id'] }),
162
+ ...(values['suite-id'] !== undefined && { suiteId: values['suite-id'] }),
163
+ ...(values['run-id'] !== undefined && { runId: values['run-id'] }),
164
+ ...(values['case-id'] !== undefined && { caseId: values['case-id'] }),
165
+ ...(values['limit'] !== undefined && { limit: values['limit'] }),
166
+ ...(values['offset'] !== undefined && { offset: values['offset'] }),
167
+ };
168
+ const bodyInput = {
169
+ ...(values['data'] !== undefined && { dataFlag: values['data'] }),
170
+ ...(values['data-file'] !== undefined && { dataFileFlag: values['data-file'] }),
171
+ // Pass a thunk (not the read contents) so resolveBody() only drains
172
+ // stdin when it actually selects stdin as the body source. Read
173
+ // actions, no-body writes (`run close`), and write actions that
174
+ // received --data or --data-file never invoke this.
175
+ ...(process.stdin.isTTY === false && { readStdin: () => readFileSync(0, 'utf-8') }),
176
+ };
177
+ const dryRun = values['dry-run'] === true;
178
+ let client;
179
+ try {
180
+ client = new TestRailClient(auth.config);
181
+ await dispatched.handler({ client, args, bodyInput, dryRun, out });
182
+ return 0;
183
+ }
184
+ catch (e) {
185
+ err(e instanceof Error ? e.message : String(e));
186
+ return 1;
187
+ }
188
+ finally {
189
+ client?.destroy();
190
+ }
191
+ }
192
+ /* v8 ignore start -- defensive: main() catches all reachable errors internally; this handler exists only for hypothetical failures (e.g., broken-pipe in process.stdout.write) that bypass the inner try/catch. */
193
+ main().then((code) => process.exit(code), (e) => {
194
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : String(e)}\n`);
195
+ process.exit(1);
196
+ });
197
+ /* v8 ignore stop */
198
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `testrail install-skill` — copy the bundled SKILL.md from this package's
3
+ * own `skill/` directory into a Claude Code skills folder so the agent can
4
+ * auto-load it.
5
+ *
6
+ * Defaults: project-scoped install (`./.claude/skills/testrail-cli/`).
7
+ * Pass `--global` for `~/.claude/skills/testrail-cli/`. Pass `--force` to
8
+ * overwrite an existing file. Pass `--print-path` to print the bundled
9
+ * source path without installing (useful for vendoring / scripting).
10
+ *
11
+ * This is a meta-command — it operates on the user's filesystem, not on
12
+ * TestRail — so it sits outside the normal `resource:action` dispatch.
13
+ * Invoked directly from `index.ts` when positionals[0] === 'install-skill'.
14
+ */
15
+ export interface InstallSkillOptions {
16
+ global: boolean;
17
+ force: boolean;
18
+ printPath: boolean;
19
+ quiet: boolean;
20
+ /** Override for tests; resolved from import.meta.url otherwise. */
21
+ sourceOverride?: string;
22
+ /** Override target root for tests; otherwise `homedir()` or `process.cwd()`. */
23
+ cwdOverride?: string;
24
+ homeOverride?: string;
25
+ }
26
+ /**
27
+ * Resolves the bundled `skill/SKILL.md` path. At runtime, the compiled
28
+ * handler lives at `<packageRoot>/dist/cli/install-skill.js`; the bundled
29
+ * skill ships in `<packageRoot>/skill/SKILL.md`. Two `..` segments climb
30
+ * from `dist/cli/` (the handler's dirname) to the package root, then
31
+ * `skill/SKILL.md` reaches the bundled file.
32
+ */
33
+ export declare function getBundledSkillPath(metaUrl: string): string;
34
+ export declare function runInstallSkill(opts: InstallSkillOptions, metaUrl: string): number;
35
+ //# sourceMappingURL=install-skill.d.ts.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `testrail install-skill` — copy the bundled SKILL.md from this package's
3
+ * own `skill/` directory into a Claude Code skills folder so the agent can
4
+ * auto-load it.
5
+ *
6
+ * Defaults: project-scoped install (`./.claude/skills/testrail-cli/`).
7
+ * Pass `--global` for `~/.claude/skills/testrail-cli/`. Pass `--force` to
8
+ * overwrite an existing file. Pass `--print-path` to print the bundled
9
+ * source path without installing (useful for vendoring / scripting).
10
+ *
11
+ * This is a meta-command — it operates on the user's filesystem, not on
12
+ * TestRail — so it sits outside the normal `resource:action` dispatch.
13
+ * Invoked directly from `index.ts` when positionals[0] === 'install-skill'.
14
+ */
15
+ import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { dirname, join, resolve } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ /**
20
+ * Resolves the bundled `skill/SKILL.md` path. At runtime, the compiled
21
+ * handler lives at `<packageRoot>/dist/cli/install-skill.js`; the bundled
22
+ * skill ships in `<packageRoot>/skill/SKILL.md`. Two `..` segments climb
23
+ * from `dist/cli/` (the handler's dirname) to the package root, then
24
+ * `skill/SKILL.md` reaches the bundled file.
25
+ */
26
+ export function getBundledSkillPath(metaUrl) {
27
+ return resolve(dirname(fileURLToPath(metaUrl)), '..', '..', 'skill', 'SKILL.md');
28
+ }
29
+ export function runInstallSkill(opts, metaUrl) {
30
+ // Match the rest of the CLI's --quiet semantics (createOutput in
31
+ // output.ts): when quiet, suppress both stdout success messages AND
32
+ // stderr errors. Callers rely on exit code 0/1 only.
33
+ const writeErr = (message) => {
34
+ if (!opts.quiet)
35
+ process.stderr.write(`Error: ${message}\n`);
36
+ };
37
+ const source = opts.sourceOverride ?? getBundledSkillPath(metaUrl);
38
+ if (opts.printPath) {
39
+ if (!opts.quiet)
40
+ process.stdout.write(`${source}\n`);
41
+ return 0;
42
+ }
43
+ if (!existsSync(source)) {
44
+ writeErr(`bundled SKILL.md not found at ${source}`);
45
+ return 1;
46
+ }
47
+ const targetRoot = opts.global ? (opts.homeOverride ?? homedir()) : (opts.cwdOverride ?? process.cwd());
48
+ const target = join(targetRoot, '.claude', 'skills', 'testrail-cli', 'SKILL.md');
49
+ if (existsSync(target) && !opts.force) {
50
+ writeErr(`SKILL.md already exists at ${target}. Re-run with --force to overwrite.`);
51
+ return 1;
52
+ }
53
+ try {
54
+ mkdirSync(dirname(target), { recursive: true });
55
+ copyFileSync(source, target);
56
+ /* v8 ignore start -- defensive: triggered only by filesystem failures
57
+ (permission denied, full disk, etc.) that are flaky to simulate in
58
+ CI. The error path is exercised manually if invoked under an
59
+ unwritable HOME. */
60
+ }
61
+ catch (e) {
62
+ writeErr(`failed to install skill: ${e instanceof Error ? e.message : String(e)}`);
63
+ return 1;
64
+ }
65
+ /* v8 ignore stop */
66
+ if (!opts.quiet) {
67
+ process.stdout.write(`Installed testrail-cli skill → ${target}\n`);
68
+ }
69
+ return 0;
70
+ }
71
+ //# sourceMappingURL=install-skill.js.map
@@ -0,0 +1,37 @@
1
+ import type { z } from 'zod';
2
+ /**
3
+ * Declarative spec for every resource:action exposed by the CLI.
4
+ *
5
+ * Single source of truth shared by:
6
+ * - PR 3 tests: assert both directions of the metadata↔dispatch
7
+ * correspondence — every `ACTIONS` entry must have a registered handler
8
+ * in `dispatch.ts` HANDLERS, and every HANDLERS key must have an
9
+ * `ACTIONS` entry. Catches drift in either direction.
10
+ * - PR 4 skill generator: renders the `<!-- GENERATED:command-table -->` and
11
+ * `<!-- GENERATED:payload-schemas -->` regions of `skill/SKILL.md` from
12
+ * this array.
13
+ *
14
+ * Adding a new action requires touching exactly two places: the handler in
15
+ * `src/cli/handlers/`, and an entry here. The dispatcher and the skill stay
16
+ * accurate automatically.
17
+ */
18
+ export interface PathParam {
19
+ name: string;
20
+ description: string;
21
+ }
22
+ export interface ActionSpec {
23
+ resource: string;
24
+ action: string;
25
+ summary: string;
26
+ pathParams: readonly PathParam[];
27
+ /** Zod schema for the request body. `undefined` for read actions and for
28
+ * no-body POSTs like `run close`. */
29
+ bodySchema?: z.ZodTypeAny;
30
+ /** True for write actions (POST / payload-bearing). Affects skill recipes,
31
+ * generator output, and `--dry-run` applicability. */
32
+ isWrite: boolean;
33
+ }
34
+ export declare const ACTIONS: readonly ActionSpec[];
35
+ /** Look up the spec for a resource:action pair, or return undefined. */
36
+ export declare function getActionSpec(resource: string, action: string): ActionSpec | undefined;
37
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1,151 @@
1
+ import { AddCasePayloadSchema, UpdateCasePayloadSchema, AddRunPayloadSchema, AddResultPayloadSchema, AddResultsForCasesPayloadSchema, } from '../schemas.js';
2
+ export const ACTIONS = [
3
+ // ── Read actions ──────────────────────────────────────────────────────
4
+ {
5
+ resource: 'project',
6
+ action: 'get',
7
+ summary: 'Fetch a single project by ID',
8
+ pathParams: [{ name: 'project_id', description: 'TestRail project ID' }],
9
+ isWrite: false,
10
+ },
11
+ {
12
+ resource: 'project',
13
+ action: 'list',
14
+ summary: 'List all projects (paginated)',
15
+ pathParams: [],
16
+ isWrite: false,
17
+ },
18
+ {
19
+ resource: 'suite',
20
+ action: 'get',
21
+ summary: 'Fetch a single suite by ID',
22
+ pathParams: [{ name: 'suite_id', description: 'TestRail suite ID' }],
23
+ isWrite: false,
24
+ },
25
+ {
26
+ resource: 'suite',
27
+ action: 'list',
28
+ summary: 'List suites in a project',
29
+ pathParams: [],
30
+ isWrite: false,
31
+ },
32
+ {
33
+ resource: 'case',
34
+ action: 'get',
35
+ summary: 'Fetch a single test case by ID',
36
+ pathParams: [{ name: 'case_id', description: 'TestRail case ID' }],
37
+ isWrite: false,
38
+ },
39
+ {
40
+ resource: 'case',
41
+ action: 'list',
42
+ summary: 'List cases in a project (optionally filtered by suite)',
43
+ pathParams: [],
44
+ isWrite: false,
45
+ },
46
+ {
47
+ resource: 'run',
48
+ action: 'get',
49
+ summary: 'Fetch a single run by ID',
50
+ pathParams: [{ name: 'run_id', description: 'TestRail run ID' }],
51
+ isWrite: false,
52
+ },
53
+ {
54
+ resource: 'run',
55
+ action: 'list',
56
+ summary: 'List runs in a project (paginated)',
57
+ pathParams: [],
58
+ isWrite: false,
59
+ },
60
+ {
61
+ resource: 'result',
62
+ action: 'list',
63
+ summary: 'List results for a run (paginated)',
64
+ pathParams: [],
65
+ isWrite: false,
66
+ },
67
+ {
68
+ resource: 'milestone',
69
+ action: 'get',
70
+ summary: 'Fetch a single milestone by ID',
71
+ pathParams: [{ name: 'milestone_id', description: 'TestRail milestone ID' }],
72
+ isWrite: false,
73
+ },
74
+ {
75
+ resource: 'milestone',
76
+ action: 'list',
77
+ summary: 'List milestones in a project (paginated)',
78
+ pathParams: [],
79
+ isWrite: false,
80
+ },
81
+ {
82
+ resource: 'user',
83
+ action: 'get',
84
+ summary: 'Fetch a single user by ID',
85
+ pathParams: [{ name: 'user_id', description: 'TestRail user ID' }],
86
+ isWrite: false,
87
+ },
88
+ {
89
+ resource: 'user',
90
+ action: 'list',
91
+ summary: 'List users (paginated)',
92
+ pathParams: [],
93
+ isWrite: false,
94
+ },
95
+ // ── Write actions ─────────────────────────────────────────────────────
96
+ {
97
+ resource: 'case',
98
+ action: 'add',
99
+ summary: 'Create a new test case under a section',
100
+ pathParams: [{ name: 'section_id', description: 'Section to create the case under' }],
101
+ bodySchema: AddCasePayloadSchema,
102
+ isWrite: true,
103
+ },
104
+ {
105
+ resource: 'case',
106
+ action: 'update',
107
+ summary: 'Update an existing test case (partial fields)',
108
+ pathParams: [{ name: 'case_id', description: 'TestRail case ID' }],
109
+ bodySchema: UpdateCasePayloadSchema,
110
+ isWrite: true,
111
+ },
112
+ {
113
+ resource: 'run',
114
+ action: 'add',
115
+ summary: 'Create a new test run in a project',
116
+ pathParams: [{ name: 'project_id', description: 'TestRail project ID' }],
117
+ bodySchema: AddRunPayloadSchema,
118
+ isWrite: true,
119
+ },
120
+ {
121
+ resource: 'run',
122
+ action: 'close',
123
+ summary: 'Close a test run (no body)',
124
+ pathParams: [{ name: 'run_id', description: 'TestRail run ID' }],
125
+ isWrite: true,
126
+ },
127
+ {
128
+ resource: 'result',
129
+ action: 'add',
130
+ summary: 'Record a single result for a case in a run',
131
+ pathParams: [
132
+ { name: 'run_id', description: 'TestRail run ID' },
133
+ { name: 'case_id', description: 'TestRail case ID' },
134
+ ],
135
+ bodySchema: AddResultPayloadSchema,
136
+ isWrite: true,
137
+ },
138
+ {
139
+ resource: 'result',
140
+ action: 'add-bulk',
141
+ summary: 'Record multiple results for cases in one API call',
142
+ pathParams: [{ name: 'run_id', description: 'TestRail run ID' }],
143
+ bodySchema: AddResultsForCasesPayloadSchema,
144
+ isWrite: true,
145
+ },
146
+ ];
147
+ /** Look up the spec for a resource:action pair, or return undefined. */
148
+ export function getActionSpec(resource, action) {
149
+ return ACTIONS.find((a) => a.resource === resource && a.action === action);
150
+ }
151
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1,28 @@
1
+ export interface OutputOptions {
2
+ quiet: boolean;
3
+ format: 'json' | 'table';
4
+ }
5
+ export interface Output {
6
+ out: (data: unknown) => void;
7
+ err: (message: string) => void;
8
+ }
9
+ export declare function valueToString(v: unknown): string;
10
+ export declare function renderTable(data: unknown): string;
11
+ /**
12
+ * Best-effort JSON.stringify with two fallbacks, guaranteeing the return
13
+ * value is always parseable JSON for downstream tools (e.g., `jq`):
14
+ *
15
+ * 1. If serialization throws (circular reference, nested BigInt, etc.),
16
+ * emit a structured `{ error, message }` JSON object.
17
+ * 2. If `JSON.stringify` returns the JS value `undefined` — which it does
18
+ * for `undefined`, function, or symbol inputs — emit the JSON literal
19
+ * `"null"`. Without this guard, the caller's template literal would
20
+ * coerce that `undefined` to the string `"undefined"`, which is not
21
+ * valid JSON.
22
+ *
23
+ * Exported so unit tests can verify the fallbacks without spawning a
24
+ * subprocess.
25
+ */
26
+ export declare function safeJsonStringify(data: unknown): string;
27
+ export declare function createOutput(opts: OutputOptions): Output;
28
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1,84 @@
1
+ export function valueToString(v) {
2
+ if (v === null || v === undefined)
3
+ return '';
4
+ if (typeof v === 'object') {
5
+ try {
6
+ return JSON.stringify(v);
7
+ }
8
+ catch {
9
+ // JSON.stringify throws on circular refs and nested BigInt.
10
+ return '[Object]';
11
+ }
12
+ }
13
+ if (typeof v === 'string')
14
+ return v;
15
+ if (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'bigint')
16
+ return String(v);
17
+ if (typeof v === 'symbol')
18
+ return v.toString();
19
+ return '[Function]';
20
+ }
21
+ function getField(row, key) {
22
+ if (typeof row !== 'object' || row === null)
23
+ return undefined;
24
+ return row[key];
25
+ }
26
+ export function renderTable(data) {
27
+ const rows = Array.isArray(data) ? data : [data];
28
+ if (rows.length === 0)
29
+ return '(empty)';
30
+ const first = rows[0];
31
+ if (typeof first !== 'object' || first === null) {
32
+ return rows.map(String).join('\n');
33
+ }
34
+ const keys = Object.keys(first);
35
+ const widths = keys.map((k) => Math.max(k.length, ...rows.map((r) => valueToString(getField(r, k)).length)));
36
+ const line = widths.map((w) => '-'.repeat(w)).join('-+-');
37
+ const header = keys.map((k, i) => k.padEnd(widths[i] ?? k.length)).join(' | ');
38
+ const body = rows.map((r) => keys.map((k, i) => valueToString(getField(r, k)).padEnd(widths[i] ?? k.length)).join(' | '));
39
+ return [header, line, ...body].join('\n');
40
+ }
41
+ /**
42
+ * Best-effort JSON.stringify with two fallbacks, guaranteeing the return
43
+ * value is always parseable JSON for downstream tools (e.g., `jq`):
44
+ *
45
+ * 1. If serialization throws (circular reference, nested BigInt, etc.),
46
+ * emit a structured `{ error, message }` JSON object.
47
+ * 2. If `JSON.stringify` returns the JS value `undefined` — which it does
48
+ * for `undefined`, function, or symbol inputs — emit the JSON literal
49
+ * `"null"`. Without this guard, the caller's template literal would
50
+ * coerce that `undefined` to the string `"undefined"`, which is not
51
+ * valid JSON.
52
+ *
53
+ * Exported so unit tests can verify the fallbacks without spawning a
54
+ * subprocess.
55
+ */
56
+ export function safeJsonStringify(data) {
57
+ try {
58
+ // JSON.stringify returns the JS value undefined for inputs without a
59
+ // JSON representation (undefined, function, symbol); fall back to the
60
+ // JSON literal "null" so the result is always a parseable string.
61
+ return JSON.stringify(data, null, 2) ?? 'null';
62
+ }
63
+ catch (e) {
64
+ return JSON.stringify({ error: 'unserializable', message: e instanceof Error ? e.message : String(e) }, null, 2);
65
+ }
66
+ }
67
+ export function createOutput(opts) {
68
+ const out = (data) => {
69
+ if (opts.quiet)
70
+ return;
71
+ if (opts.format === 'table') {
72
+ process.stdout.write(`${renderTable(data)}\n`);
73
+ }
74
+ else {
75
+ process.stdout.write(`${safeJsonStringify(data)}\n`);
76
+ }
77
+ };
78
+ const err = (message) => {
79
+ if (!opts.quiet)
80
+ process.stderr.write(`Error: ${message}\n`);
81
+ };
82
+ return { out, err };
83
+ }
84
+ //# sourceMappingURL=output.js.map
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import './cli/index.js';
3
3
  //# sourceMappingURL=cli.d.ts.map