@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,13 @@
1
+ import type { HandlerContext } from '../handler-context.js';
2
+ import { parseId, optInt } from '../ids.js';
3
+
4
+ export async function handleCaseGet(ctx: HandlerContext): Promise<void> {
5
+ const id = parseId(ctx.args.pathParams[0], 'case id');
6
+ ctx.out(await ctx.client.getCase(id));
7
+ }
8
+
9
+ export async function handleCaseList(ctx: HandlerContext): Promise<void> {
10
+ const pid = parseId(ctx.args.projectId, '--project-id');
11
+ const suiteId = optInt(ctx.args.suiteId);
12
+ ctx.out(await ctx.client.getCases(pid, suiteId !== undefined ? { suiteId } : undefined));
13
+ }
@@ -0,0 +1,19 @@
1
+ import type { HandlerContext } from '../handler-context.js';
2
+ import { parseId, optInt } from '../ids.js';
3
+
4
+ export async function handleMilestoneGet(ctx: HandlerContext): Promise<void> {
5
+ const id = parseId(ctx.args.pathParams[0], 'milestone id');
6
+ ctx.out(await ctx.client.getMilestone(id));
7
+ }
8
+
9
+ export async function handleMilestoneList(ctx: HandlerContext): Promise<void> {
10
+ const pid = parseId(ctx.args.projectId, '--project-id');
11
+ const limit = optInt(ctx.args.limit);
12
+ const offset = optInt(ctx.args.offset);
13
+ ctx.out(
14
+ await ctx.client.getMilestones(pid, {
15
+ ...(limit !== undefined && { limit }),
16
+ ...(offset !== undefined && { offset }),
17
+ }),
18
+ );
19
+ }
@@ -0,0 +1,13 @@
1
+ import type { HandlerContext } from '../handler-context.js';
2
+ import { parseId, optInt } from '../ids.js';
3
+
4
+ export async function handleProjectGet(ctx: HandlerContext): Promise<void> {
5
+ const id = parseId(ctx.args.pathParams[0], 'project id');
6
+ ctx.out(await ctx.client.getProject(id));
7
+ }
8
+
9
+ export async function handleProjectList(ctx: HandlerContext): Promise<void> {
10
+ const limit = optInt(ctx.args.limit);
11
+ const offset = optInt(ctx.args.offset);
12
+ ctx.out(await ctx.client.getProjects(limit, offset));
13
+ }
@@ -0,0 +1,40 @@
1
+ import type { HandlerContext } from '../handler-context.js';
2
+ import { parseId } from '../ids.js';
3
+ import { resolveBody } from '../body.js';
4
+ import { AddResultPayloadSchema, AddResultsForCasesPayloadSchema } from '../../schemas.js';
5
+
6
+ export async function handleResultAdd(ctx: HandlerContext): Promise<void> {
7
+ const runId = parseId(ctx.args.pathParams[0], 'run_id');
8
+ const caseId = parseId(ctx.args.pathParams[1], 'case_id');
9
+ const body = resolveBody(ctx.bodyInput, AddResultPayloadSchema);
10
+ if (!body.ok) throw new Error(body.error);
11
+ if (ctx.dryRun) {
12
+ ctx.out({
13
+ dryRun: true,
14
+ action: 'result add',
15
+ runId,
16
+ caseId,
17
+ payload: body.payload,
18
+ source: body.source,
19
+ });
20
+ return;
21
+ }
22
+ ctx.out(await ctx.client.addResultForCase(runId, caseId, body.payload));
23
+ }
24
+
25
+ export async function handleResultAddBulk(ctx: HandlerContext): Promise<void> {
26
+ const runId = parseId(ctx.args.pathParams[0], 'run_id');
27
+ const body = resolveBody(ctx.bodyInput, AddResultsForCasesPayloadSchema);
28
+ if (!body.ok) throw new Error(body.error);
29
+ if (ctx.dryRun) {
30
+ ctx.out({
31
+ dryRun: true,
32
+ action: 'result add-bulk',
33
+ runId,
34
+ payload: body.payload,
35
+ source: body.source,
36
+ });
37
+ return;
38
+ }
39
+ ctx.out(await ctx.client.addResultsForCases(runId, body.payload));
40
+ }
@@ -0,0 +1,14 @@
1
+ import type { HandlerContext } from '../handler-context.js';
2
+ import { parseId, optInt } from '../ids.js';
3
+
4
+ export async function handleResultList(ctx: HandlerContext): Promise<void> {
5
+ const rid = parseId(ctx.args.runId, '--run-id');
6
+ const limit = optInt(ctx.args.limit);
7
+ const offset = optInt(ctx.args.offset);
8
+ ctx.out(
9
+ await ctx.client.getResultsForRun(rid, {
10
+ ...(limit !== undefined && { limit }),
11
+ ...(offset !== undefined && { offset }),
12
+ }),
13
+ );
14
+ }
@@ -0,0 +1,30 @@
1
+ import type { HandlerContext } from '../handler-context.js';
2
+ import { parseId } from '../ids.js';
3
+ import { resolveBody } from '../body.js';
4
+ import { AddRunPayloadSchema } from '../../schemas.js';
5
+
6
+ export async function handleRunAdd(ctx: HandlerContext): Promise<void> {
7
+ const projectId = parseId(ctx.args.pathParams[0], 'project_id');
8
+ const body = resolveBody(ctx.bodyInput, AddRunPayloadSchema);
9
+ if (!body.ok) throw new Error(body.error);
10
+ if (ctx.dryRun) {
11
+ ctx.out({ dryRun: true, action: 'run add', projectId, payload: body.payload, source: body.source });
12
+ return;
13
+ }
14
+ ctx.out(await ctx.client.addRun(projectId, body.payload));
15
+ }
16
+
17
+ /**
18
+ * Close a run. Unlike the other write actions this takes no body — just a
19
+ * single `run_id` path param. POST has no payload, so the body-source
20
+ * resolver is not consulted; any `--data` / `--data-file` / stdin supplied
21
+ * for this action is silently ignored.
22
+ */
23
+ export async function handleRunClose(ctx: HandlerContext): Promise<void> {
24
+ const runId = parseId(ctx.args.pathParams[0], 'run_id');
25
+ if (ctx.dryRun) {
26
+ ctx.out({ dryRun: true, action: 'run close', runId });
27
+ return;
28
+ }
29
+ ctx.out(await ctx.client.closeRun(runId));
30
+ }
@@ -0,0 +1,19 @@
1
+ import type { HandlerContext } from '../handler-context.js';
2
+ import { parseId, optInt } from '../ids.js';
3
+
4
+ export async function handleRunGet(ctx: HandlerContext): Promise<void> {
5
+ const id = parseId(ctx.args.pathParams[0], 'run id');
6
+ ctx.out(await ctx.client.getRun(id));
7
+ }
8
+
9
+ export async function handleRunList(ctx: HandlerContext): Promise<void> {
10
+ const pid = parseId(ctx.args.projectId, '--project-id');
11
+ const limit = optInt(ctx.args.limit);
12
+ const offset = optInt(ctx.args.offset);
13
+ ctx.out(
14
+ await ctx.client.getRuns(pid, {
15
+ ...(limit !== undefined && { limit }),
16
+ ...(offset !== undefined && { offset }),
17
+ }),
18
+ );
19
+ }
@@ -0,0 +1,12 @@
1
+ import type { HandlerContext } from '../handler-context.js';
2
+ import { parseId } from '../ids.js';
3
+
4
+ export async function handleSuiteGet(ctx: HandlerContext): Promise<void> {
5
+ const id = parseId(ctx.args.pathParams[0], 'suite id');
6
+ ctx.out(await ctx.client.getSuite(id));
7
+ }
8
+
9
+ export async function handleSuiteList(ctx: HandlerContext): Promise<void> {
10
+ const pid = parseId(ctx.args.projectId, '--project-id');
11
+ ctx.out(await ctx.client.getSuites(pid));
12
+ }
@@ -0,0 +1,13 @@
1
+ import type { HandlerContext } from '../handler-context.js';
2
+ import { parseId, optInt } from '../ids.js';
3
+
4
+ export async function handleUserGet(ctx: HandlerContext): Promise<void> {
5
+ const id = parseId(ctx.args.pathParams[0], 'user id');
6
+ ctx.out(await ctx.client.getUser(id));
7
+ }
8
+
9
+ export async function handleUserList(ctx: HandlerContext): Promise<void> {
10
+ const limit = optInt(ctx.args.limit);
11
+ const offset = optInt(ctx.args.offset);
12
+ ctx.out(await ctx.client.getUsers(limit, offset));
13
+ }
package/src/cli/ids.ts ADDED
@@ -0,0 +1,20 @@
1
+ export class IdParseError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'IdParseError';
5
+ }
6
+ }
7
+
8
+ export function parseId(raw: string | undefined, name: string): number {
9
+ const n = Number(raw);
10
+ if (raw === undefined || raw === '' || !Number.isInteger(n) || n <= 0) {
11
+ throw new IdParseError(`${name} must be a positive integer (got: ${raw ?? '(none)'})`);
12
+ }
13
+ return n;
14
+ }
15
+
16
+ export function optInt(raw: string | undefined): number | undefined {
17
+ if (raw === undefined) return undefined;
18
+ const n = Number(raw);
19
+ return Number.isInteger(n) && n >= 0 ? n : undefined;
20
+ }
@@ -0,0 +1,224 @@
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
+ import type { BodyInput, HandlerArgs } from './handler-context.js';
11
+
12
+ // ── Version ───────────────────────────────────────────────────────────────────
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const VERSION: string = (require('../../package.json') as { version: string }).version;
16
+
17
+ // ── Help ──────────────────────────────────────────────────────────────────────
18
+
19
+ const HELP = `
20
+ testrail <resource> <action> [args] [options]
21
+
22
+ Read actions:
23
+ project get <id> | list [--limit N] [--offset N]
24
+ suite get <id> | list --project-id <id>
25
+ case get <id> | list --project-id <id> [--suite-id <id>]
26
+ run get <id> | list --project-id <id> [--limit N] [--offset N]
27
+ result list --run-id <id> [--limit N] [--offset N]
28
+ milestone get <id> | list --project-id <id> [--limit N] [--offset N]
29
+ user get <id> | list [--limit N] [--offset N]
30
+
31
+ Write actions (body via --data | --data-file | stdin):
32
+ case add <section_id> --data '{"title":"..."}'
33
+ case update <case_id> --data '{"title":"..."}'
34
+ run add <project_id> --data '{"name":"..."}'
35
+ run close <run_id> (no body)
36
+ result add <run_id> <case_id> --data '{"status_id":1}'
37
+ result add-bulk <run_id> --data '{"results":[{"case_id":1,"status_id":1}]}'
38
+
39
+ Meta:
40
+ install-skill [--global] [--force] [--print-path]
41
+ Install the testrail-cli skill to
42
+ ./.claude/skills/testrail-cli (default)
43
+ or ~/.claude/skills/testrail-cli (--global)
44
+
45
+ Auth (env var or flag):
46
+ TESTRAIL_BASE_URL / --base-url <url>
47
+ TESTRAIL_EMAIL / --email <email>
48
+ TESTRAIL_API_KEY / --api-key <key>
49
+
50
+ Options:
51
+ --data <json> Inline JSON body for write actions
52
+ --data-file <path> Read JSON body from file
53
+ --dry-run Validate payload but don't call the API
54
+ --format json|table Output format (default: json)
55
+ --quiet Suppress output; use exit code 0/1
56
+ --global install-skill: install to ~/.claude/skills/ (default: ./.claude/skills/)
57
+ --force install-skill: overwrite an existing SKILL.md
58
+ --print-path install-skill: print bundled SKILL.md path and exit
59
+ --help Show this help
60
+ --version Print version
61
+
62
+ For body-bearing write actions (all except 'run close'), exactly one body source
63
+ is required (--data | --data-file | stdin). Stdin is auto-detected when input
64
+ is piped (process.stdin.isTTY === false).
65
+ `.trim();
66
+
67
+ // ── Entry Point ───────────────────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Compute exit code in an async function and apply `process.exit()` once
71
+ * at the very end. parseArgs and createOutput are invoked inside main() so
72
+ * any failure during initialization (e.g. an invalid CLI shape that makes
73
+ * parseArgs throw) is funneled through the same exit-code return path
74
+ * rather than escaping as an uncaught module-evaluation error.
75
+ */
76
+ async function main(): Promise<number> {
77
+ let values: Record<string, unknown>;
78
+ let positionals: string[];
79
+ try {
80
+ const parsed = parseArgs({
81
+ args: process.argv.slice(2),
82
+ options: {
83
+ 'base-url': { type: 'string' },
84
+ email: { type: 'string' },
85
+ 'api-key': { type: 'string' },
86
+ format: { type: 'string', default: 'json' },
87
+ quiet: { type: 'boolean', default: false },
88
+ help: { type: 'boolean', default: false },
89
+ version: { type: 'boolean', default: false },
90
+ 'project-id': { type: 'string' },
91
+ 'suite-id': { type: 'string' },
92
+ 'run-id': { type: 'string' },
93
+ 'case-id': { type: 'string' },
94
+ limit: { type: 'string' },
95
+ offset: { type: 'string' },
96
+ data: { type: 'string' },
97
+ 'data-file': { type: 'string' },
98
+ 'dry-run': { type: 'boolean', default: false },
99
+ global: { type: 'boolean', default: false },
100
+ force: { type: 'boolean', default: false },
101
+ 'print-path': { type: 'boolean', default: false },
102
+ },
103
+ allowPositionals: true,
104
+ strict: false,
105
+ });
106
+ values = parsed.values;
107
+ positionals = parsed.positionals;
108
+ /* v8 ignore start -- defensive: parseArgs with strict:false is highly
109
+ tolerant; this catch funnels any future-Node-version edge cases
110
+ through the controlled exit path rather than crashing the module. */
111
+ } catch (e: unknown) {
112
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : String(e)}\n`);
113
+ return 1;
114
+ }
115
+ /* v8 ignore stop */
116
+
117
+ const quiet = values['quiet'] === true;
118
+ const formatRaw = values['format'];
119
+ const format: 'json' | 'table' = formatRaw === 'table' ? 'table' : 'json';
120
+ const { out, err } = createOutput({ quiet, format });
121
+
122
+ if (values['version'] === true) {
123
+ process.stdout.write(`testrail-cli v${VERSION}\n`);
124
+ return 0;
125
+ }
126
+
127
+ if (values['help'] === true || positionals.length === 0) {
128
+ process.stdout.write(`${HELP}\n`);
129
+ return 0;
130
+ }
131
+
132
+ // `install-skill` is a meta-command (manages the bundled skill on the
133
+ // user's filesystem). It deliberately sits outside the normal
134
+ // resource:action dispatch since there is no API call involved.
135
+ if (positionals[0] === 'install-skill') {
136
+ return runInstallSkill(
137
+ {
138
+ global: values['global'] === true,
139
+ force: values['force'] === true,
140
+ printPath: values['print-path'] === true,
141
+ quiet,
142
+ },
143
+ import.meta.url,
144
+ );
145
+ }
146
+
147
+ const [resource, action, ...rest] = positionals;
148
+ const pathParams: readonly string[] = rest;
149
+
150
+ if (resource === undefined || resource === '' || action === undefined || action === '') {
151
+ process.stderr.write('Usage: testrail <resource> <action> [args] [options]\nRun with --help for details.\n');
152
+ return 1;
153
+ }
154
+
155
+ const dispatched = dispatch(resource, action);
156
+ if (!dispatched.ok) {
157
+ err(dispatched.error);
158
+ return 1;
159
+ }
160
+
161
+ const auth = resolveAuth(
162
+ {
163
+ baseUrl: values['base-url'] as string | undefined,
164
+ email: values['email'] as string | undefined,
165
+ apiKey: values['api-key'] as string | undefined,
166
+ },
167
+ {
168
+ ...(process.env['TESTRAIL_BASE_URL'] !== undefined && {
169
+ TESTRAIL_BASE_URL: process.env['TESTRAIL_BASE_URL'],
170
+ }),
171
+ ...(process.env['TESTRAIL_EMAIL'] !== undefined && { TESTRAIL_EMAIL: process.env['TESTRAIL_EMAIL'] }),
172
+ ...(process.env['TESTRAIL_API_KEY'] !== undefined && { TESTRAIL_API_KEY: process.env['TESTRAIL_API_KEY'] }),
173
+ },
174
+ );
175
+
176
+ if (!auth.ok) {
177
+ err(auth.error);
178
+ return 1;
179
+ }
180
+
181
+ const args: HandlerArgs = {
182
+ pathParams,
183
+ ...(values['project-id'] !== undefined && { projectId: values['project-id'] as string }),
184
+ ...(values['suite-id'] !== undefined && { suiteId: values['suite-id'] as string }),
185
+ ...(values['run-id'] !== undefined && { runId: values['run-id'] as string }),
186
+ ...(values['case-id'] !== undefined && { caseId: values['case-id'] as string }),
187
+ ...(values['limit'] !== undefined && { limit: values['limit'] as string }),
188
+ ...(values['offset'] !== undefined && { offset: values['offset'] as string }),
189
+ };
190
+
191
+ const bodyInput: BodyInput = {
192
+ ...(values['data'] !== undefined && { dataFlag: values['data'] as string }),
193
+ ...(values['data-file'] !== undefined && { dataFileFlag: values['data-file'] as string }),
194
+ // Pass a thunk (not the read contents) so resolveBody() only drains
195
+ // stdin when it actually selects stdin as the body source. Read
196
+ // actions, no-body writes (`run close`), and write actions that
197
+ // received --data or --data-file never invoke this.
198
+ ...(process.stdin.isTTY === false && { readStdin: () => readFileSync(0, 'utf-8') }),
199
+ };
200
+
201
+ const dryRun = values['dry-run'] === true;
202
+
203
+ let client: TestRailClient | undefined;
204
+ try {
205
+ client = new TestRailClient(auth.config);
206
+ await dispatched.handler({ client, args, bodyInput, dryRun, out });
207
+ return 0;
208
+ } catch (e: unknown) {
209
+ err(e instanceof Error ? e.message : String(e));
210
+ return 1;
211
+ } finally {
212
+ client?.destroy();
213
+ }
214
+ }
215
+
216
+ /* 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. */
217
+ main().then(
218
+ (code) => process.exit(code),
219
+ (e: unknown) => {
220
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : String(e)}\n`);
221
+ process.exit(1);
222
+ },
223
+ );
224
+ /* v8 ignore stop */
@@ -0,0 +1,89 @@
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
+
16
+ import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
17
+ import { homedir } from 'node:os';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ export interface InstallSkillOptions {
22
+ global: boolean;
23
+ force: boolean;
24
+ printPath: boolean;
25
+ quiet: boolean;
26
+ /** Override for tests; resolved from import.meta.url otherwise. */
27
+ sourceOverride?: string;
28
+ /** Override target root for tests; otherwise `homedir()` or `process.cwd()`. */
29
+ cwdOverride?: string;
30
+ homeOverride?: string;
31
+ }
32
+
33
+ /**
34
+ * Resolves the bundled `skill/SKILL.md` path. At runtime, the compiled
35
+ * handler lives at `<packageRoot>/dist/cli/install-skill.js`; the bundled
36
+ * skill ships in `<packageRoot>/skill/SKILL.md`. Two `..` segments climb
37
+ * from `dist/cli/` (the handler's dirname) to the package root, then
38
+ * `skill/SKILL.md` reaches the bundled file.
39
+ */
40
+ export function getBundledSkillPath(metaUrl: string): string {
41
+ return resolve(dirname(fileURLToPath(metaUrl)), '..', '..', 'skill', 'SKILL.md');
42
+ }
43
+
44
+ export function runInstallSkill(opts: InstallSkillOptions, metaUrl: string): number {
45
+ // Match the rest of the CLI's --quiet semantics (createOutput in
46
+ // output.ts): when quiet, suppress both stdout success messages AND
47
+ // stderr errors. Callers rely on exit code 0/1 only.
48
+ const writeErr = (message: string): void => {
49
+ if (!opts.quiet) process.stderr.write(`Error: ${message}\n`);
50
+ };
51
+
52
+ const source = opts.sourceOverride ?? getBundledSkillPath(metaUrl);
53
+
54
+ if (opts.printPath) {
55
+ if (!opts.quiet) process.stdout.write(`${source}\n`);
56
+ return 0;
57
+ }
58
+
59
+ if (!existsSync(source)) {
60
+ writeErr(`bundled SKILL.md not found at ${source}`);
61
+ return 1;
62
+ }
63
+
64
+ const targetRoot = opts.global ? (opts.homeOverride ?? homedir()) : (opts.cwdOverride ?? process.cwd());
65
+ const target = join(targetRoot, '.claude', 'skills', 'testrail-cli', 'SKILL.md');
66
+
67
+ if (existsSync(target) && !opts.force) {
68
+ writeErr(`SKILL.md already exists at ${target}. Re-run with --force to overwrite.`);
69
+ return 1;
70
+ }
71
+
72
+ try {
73
+ mkdirSync(dirname(target), { recursive: true });
74
+ copyFileSync(source, target);
75
+ /* v8 ignore start -- defensive: triggered only by filesystem failures
76
+ (permission denied, full disk, etc.) that are flaky to simulate in
77
+ CI. The error path is exercised manually if invoked under an
78
+ unwritable HOME. */
79
+ } catch (e: unknown) {
80
+ writeErr(`failed to install skill: ${e instanceof Error ? e.message : String(e)}`);
81
+ return 1;
82
+ }
83
+ /* v8 ignore stop */
84
+
85
+ if (!opts.quiet) {
86
+ process.stdout.write(`Installed testrail-cli skill → ${target}\n`);
87
+ }
88
+ return 0;
89
+ }