@dichovsky/testrail-api-client 1.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/cli/auth.d.ts +21 -0
- package/dist/cli/auth.js +16 -0
- package/dist/cli/body.d.ts +42 -0
- package/dist/cli/body.js +89 -0
- package/dist/cli/dispatch.d.ts +16 -0
- package/dist/cli/dispatch.js +87 -0
- package/dist/cli/handler-context.d.ts +43 -0
- package/dist/cli/handler-context.js +2 -0
- package/dist/cli/handlers/case-write.d.ts +4 -0
- package/dist/cli/handlers/case-write.js +26 -0
- package/dist/cli/handlers/case.d.ts +4 -0
- package/dist/cli/handlers/case.js +11 -0
- package/dist/cli/handlers/milestone.d.ts +4 -0
- package/dist/cli/handlers/milestone.js +15 -0
- package/dist/cli/handlers/project.d.ts +4 -0
- package/dist/cli/handlers/project.js +11 -0
- package/dist/cli/handlers/result-write.d.ts +4 -0
- package/dist/cli/handlers/result-write.js +40 -0
- package/dist/cli/handlers/result.d.ts +3 -0
- package/dist/cli/handlers/result.js +11 -0
- package/dist/cli/handlers/run-write.d.ts +10 -0
- package/dist/cli/handlers/run-write.js +29 -0
- package/dist/cli/handlers/run.d.ts +4 -0
- package/dist/cli/handlers/run.js +15 -0
- package/dist/cli/handlers/suite.d.ts +4 -0
- package/dist/cli/handlers/suite.js +10 -0
- package/dist/cli/handlers/user.d.ts +4 -0
- package/dist/cli/handlers/user.js +11 -0
- package/dist/cli/ids.d.ts +6 -0
- package/dist/cli/ids.js +20 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +198 -0
- package/dist/cli/install-skill.d.ts +35 -0
- package/dist/cli/install-skill.js +71 -0
- package/dist/cli/metadata.d.ts +37 -0
- package/dist/cli/metadata.js +151 -0
- package/dist/cli/output.d.ts +28 -0
- package/dist/cli/output.js +84 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -266
- package/dist/client-core.d.ts +16 -7
- package/dist/client-core.js +153 -27
- package/dist/client.d.ts +274 -118
- package/dist/client.js +404 -463
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/errors.d.ts +11 -9
- package/dist/errors.js +12 -8
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/modules/attachments.d.ts +19 -0
- package/dist/modules/attachments.js +64 -0
- package/dist/modules/cases.d.ts +13 -0
- package/dist/modules/cases.js +58 -0
- package/dist/modules/configurations.d.ts +14 -0
- package/dist/modules/configurations.js +37 -0
- package/dist/modules/datasets.d.ts +12 -0
- package/dist/modules/datasets.js +28 -0
- package/dist/modules/metadata.d.ts +14 -0
- package/dist/modules/metadata.js +31 -0
- package/dist/modules/milestones.d.ts +12 -0
- package/dist/modules/milestones.js +36 -0
- package/dist/modules/plans.d.ts +16 -0
- package/dist/modules/plans.js +59 -0
- package/dist/modules/projects.d.ts +36 -0
- package/dist/modules/projects.js +55 -0
- package/dist/modules/reports.d.ts +9 -0
- package/dist/modules/reports.js +16 -0
- package/dist/modules/results.d.ts +14 -0
- package/dist/modules/results.js +69 -0
- package/dist/modules/runs.d.ts +14 -0
- package/dist/modules/runs.js +57 -0
- package/dist/modules/sections.d.ts +16 -0
- package/dist/modules/sections.js +37 -0
- package/dist/modules/sharedSteps.d.ts +12 -0
- package/dist/modules/sharedSteps.js +28 -0
- package/dist/modules/suites.d.ts +37 -0
- package/dist/modules/suites.js +54 -0
- package/dist/modules/tests.d.ts +9 -0
- package/dist/modules/tests.js +25 -0
- package/dist/modules/users.d.ts +18 -0
- package/dist/modules/users.js +62 -0
- package/dist/modules/variables.d.ts +11 -0
- package/dist/modules/variables.js +24 -0
- package/dist/schemas.d.ts +544 -0
- package/dist/schemas.js +419 -0
- package/dist/types.d.ts +1 -55
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +4 -0
- package/package.json +23 -15
- package/skill/SKILL.md +395 -0
- package/src/cli/auth.ts +37 -0
- package/src/cli/body.ts +100 -0
- package/src/cli/dispatch.ts +91 -0
- package/src/cli/handler-context.ts +46 -0
- package/src/cli/handlers/case-write.ts +26 -0
- package/src/cli/handlers/case.ts +13 -0
- package/src/cli/handlers/milestone.ts +19 -0
- package/src/cli/handlers/project.ts +13 -0
- package/src/cli/handlers/result-write.ts +40 -0
- package/src/cli/handlers/result.ts +14 -0
- package/src/cli/handlers/run-write.ts +30 -0
- package/src/cli/handlers/run.ts +19 -0
- package/src/cli/handlers/suite.ts +12 -0
- package/src/cli/handlers/user.ts +13 -0
- package/src/cli/ids.ts +20 -0
- package/src/cli/index.ts +224 -0
- package/src/cli/install-skill.ts +89 -0
- package/src/cli/metadata.ts +194 -0
- package/src/cli/output.ts +96 -0
- package/src/cli.ts +1 -286
- package/src/client-core.ts +183 -67
- package/src/client.ts +414 -483
- package/src/constants.ts +1 -0
- package/src/errors.ts +18 -11
- package/src/index.ts +50 -8
- package/src/modules/attachments.ts +125 -0
- package/src/modules/cases.ts +78 -0
- package/src/modules/configurations.ts +68 -0
- package/src/modules/datasets.ts +44 -0
- package/src/modules/metadata.ts +63 -0
- package/src/modules/milestones.ts +54 -0
- package/src/modules/plans.ts +89 -0
- package/src/modules/projects.ts +67 -0
- package/src/modules/reports.ts +23 -0
- package/src/modules/results.ts +90 -0
- package/src/modules/runs.ts +70 -0
- package/src/modules/sections.ts +55 -0
- package/src/modules/sharedSteps.ts +44 -0
- package/src/modules/suites.ts +67 -0
- package/src/modules/tests.ts +28 -0
- package/src/modules/users.ts +87 -0
- package/src/modules/variables.ts +36 -0
- package/src/schemas.ts +551 -0
- package/src/types.ts +11 -60
- package/src/utils.ts +5 -0
|
@@ -0,0 +1,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
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -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
|
+
}
|