@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,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