@automatify-au/cli 0.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 (3) hide show
  1. package/README.md +389 -0
  2. package/dist/automatify.js +4011 -0
  3. package/package.json +59 -0
@@ -0,0 +1,4011 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cliResult.ts
4
+ var ExitCode = {
5
+ Success: 0,
6
+ InternalError: 1,
7
+ UsageError: 2,
8
+ ValidationError: 3,
9
+ RemoteError: 4,
10
+ TransportError: 5
11
+ };
12
+ function toJsonLine(data) {
13
+ return [JSON.stringify(data)];
14
+ }
15
+
16
+ // src/auto.ts
17
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
18
+ import path5 from "node:path";
19
+
20
+ // ../../packages/shared-types/dist/index.js
21
+ var TestCaseStatus;
22
+ (function(TestCaseStatus2) {
23
+ TestCaseStatus2["Draft"] = "draft";
24
+ TestCaseStatus2["Active"] = "active";
25
+ TestCaseStatus2["Deprecated"] = "deprecated";
26
+ })(TestCaseStatus || (TestCaseStatus = {}));
27
+ var StepResult;
28
+ (function(StepResult2) {
29
+ StepResult2["Passed"] = "passed";
30
+ StepResult2["Failed"] = "failed";
31
+ StepResult2["Skipped"] = "skipped";
32
+ StepResult2["Blocked"] = "blocked";
33
+ })(StepResult || (StepResult = {}));
34
+ var ExecutionCycleStatus;
35
+ (function(ExecutionCycleStatus2) {
36
+ ExecutionCycleStatus2["Draft"] = "draft";
37
+ ExecutionCycleStatus2["Active"] = "active";
38
+ ExecutionCycleStatus2["Completed"] = "completed";
39
+ ExecutionCycleStatus2["Archived"] = "archived";
40
+ })(ExecutionCycleStatus || (ExecutionCycleStatus = {}));
41
+ var ExecutionPlanningStatus;
42
+ (function(ExecutionPlanningStatus2) {
43
+ ExecutionPlanningStatus2["NotRun"] = "notRun";
44
+ ExecutionPlanningStatus2["InProgress"] = "inProgress";
45
+ ExecutionPlanningStatus2["Passed"] = "passed";
46
+ ExecutionPlanningStatus2["Failed"] = "failed";
47
+ ExecutionPlanningStatus2["Blocked"] = "blocked";
48
+ ExecutionPlanningStatus2["Skipped"] = "skipped";
49
+ })(ExecutionPlanningStatus || (ExecutionPlanningStatus = {}));
50
+ var Priority;
51
+ (function(Priority2) {
52
+ Priority2["Low"] = "low";
53
+ Priority2["Medium"] = "medium";
54
+ Priority2["High"] = "high";
55
+ })(Priority || (Priority = {}));
56
+
57
+ // src/config.ts
58
+ import { existsSync, readFileSync } from "node:fs";
59
+ import path from "node:path";
60
+ var DEFAULT_CONFIG_FILENAME = ".testops-cli.json";
61
+ function parseFlags(args) {
62
+ const flags = {};
63
+ const unknownFlags = [];
64
+ const supported = /* @__PURE__ */ new Set([
65
+ "--config",
66
+ "--base-url",
67
+ "--project-key",
68
+ "--issue-key",
69
+ "--auth-mode",
70
+ "--jira-email",
71
+ "--jira-api-token"
72
+ ]);
73
+ for (let i = 0; i < args.length; i += 1) {
74
+ const token = args[i];
75
+ if (!token.startsWith("--")) {
76
+ continue;
77
+ }
78
+ if (!supported.has(token)) {
79
+ unknownFlags.push(token);
80
+ continue;
81
+ }
82
+ const value = args[i + 1];
83
+ if (!value || value.startsWith("--")) {
84
+ continue;
85
+ }
86
+ flags[token] = value;
87
+ i += 1;
88
+ }
89
+ return { flags, unknownFlags };
90
+ }
91
+ function toStringValue(value) {
92
+ return typeof value === "string" ? value.trim() : "";
93
+ }
94
+ function readConfigFile(configPath) {
95
+ if (!existsSync(configPath)) {
96
+ return {};
97
+ }
98
+ try {
99
+ const raw = readFileSync(configPath, "utf8");
100
+ const parsed = JSON.parse(raw);
101
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
102
+ } catch {
103
+ return {};
104
+ }
105
+ }
106
+ function normalizeBaseUrl(value) {
107
+ if (value.endsWith("/")) {
108
+ return value.slice(0, -1);
109
+ }
110
+ return value;
111
+ }
112
+ function readAuthMode(input) {
113
+ return input === "api-token" ? "api-token" : "none";
114
+ }
115
+ function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "") {
116
+ if (flagValue) {
117
+ return { value: flagValue, source: "flag" };
118
+ }
119
+ if (envValue) {
120
+ return { value: envValue, source: "env" };
121
+ }
122
+ if (fileValue) {
123
+ return { value: fileValue, source: "file" };
124
+ }
125
+ if (fallback) {
126
+ return { value: fallback, source: "default" };
127
+ }
128
+ return { value: "", source: "default" };
129
+ }
130
+ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
131
+ const { flags, unknownFlags } = parseFlags(args);
132
+ const configPathFlag = flags["--config"] ? path.resolve(cwd, flags["--config"]) : "";
133
+ const configPathEnv = toStringValue(env.TESTOPS_CONFIG_PATH);
134
+ const configPath = configPathFlag || (configPathEnv ? path.resolve(cwd, configPathEnv) : path.resolve(cwd, DEFAULT_CONFIG_FILENAME));
135
+ const configPathSource = configPathFlag ? "flag" : configPathEnv ? "env" : "default";
136
+ const file = readConfigFile(configPath);
137
+ const baseUrlResolved = valueFromPrecedence(
138
+ "baseUrl",
139
+ toStringValue(flags["--base-url"]),
140
+ toStringValue(env.JIRA_BASE_URL),
141
+ toStringValue(file.baseUrl ?? file.JIRA_BASE_URL)
142
+ );
143
+ const projectKeyResolved = valueFromPrecedence(
144
+ "projectKey",
145
+ toStringValue(flags["--project-key"]),
146
+ toStringValue(env.JIRA_PROJECT_KEY),
147
+ toStringValue(file.projectKey ?? file.JIRA_PROJECT_KEY)
148
+ );
149
+ const issueKeyResolved = valueFromPrecedence(
150
+ "issueKey",
151
+ toStringValue(flags["--issue-key"]),
152
+ toStringValue(env.JIRA_ISSUE_KEY),
153
+ toStringValue(file.issueKey ?? file.JIRA_ISSUE_KEY)
154
+ );
155
+ const authModeResolved = valueFromPrecedence(
156
+ "authMode",
157
+ toStringValue(flags["--auth-mode"]),
158
+ toStringValue(env.TESTOPS_AUTH_MODE),
159
+ toStringValue(file.authMode ?? file.TESTOPS_AUTH_MODE),
160
+ "none"
161
+ );
162
+ const jiraEmailResolved = valueFromPrecedence(
163
+ "jiraEmail",
164
+ toStringValue(flags["--jira-email"]),
165
+ toStringValue(env.JIRA_EMAIL),
166
+ toStringValue(file.jiraEmail ?? file.JIRA_EMAIL)
167
+ );
168
+ const jiraApiTokenResolved = valueFromPrecedence(
169
+ "jiraApiToken",
170
+ toStringValue(flags["--jira-api-token"]),
171
+ toStringValue(env.JIRA_API_TOKEN),
172
+ toStringValue(file.jiraApiToken ?? file.JIRA_API_TOKEN)
173
+ );
174
+ const config = {
175
+ baseUrl: normalizeBaseUrl(baseUrlResolved.value),
176
+ projectKey: projectKeyResolved.value,
177
+ issueKey: issueKeyResolved.value,
178
+ authMode: readAuthMode(authModeResolved.value),
179
+ jiraEmail: jiraEmailResolved.value,
180
+ jiraApiToken: jiraApiTokenResolved.value,
181
+ configPath
182
+ };
183
+ const sources = {
184
+ baseUrl: baseUrlResolved.source,
185
+ projectKey: projectKeyResolved.source,
186
+ issueKey: issueKeyResolved.source,
187
+ authMode: authModeResolved.source,
188
+ jiraEmail: jiraEmailResolved.source,
189
+ jiraApiToken: jiraApiTokenResolved.source,
190
+ configPath: configPathSource
191
+ };
192
+ const warnings = [];
193
+ if (unknownFlags.length > 0) {
194
+ warnings.push(`Ignored unknown config flags: ${unknownFlags.join(", ")}`);
195
+ }
196
+ if (!existsSync(configPath) && sources.configPath !== "default") {
197
+ warnings.push(`Config file not found at ${configPath}; using env/flags/defaults.`);
198
+ }
199
+ return { values: config, sources, warnings };
200
+ }
201
+ function maskSecret(value) {
202
+ if (!value) {
203
+ return "";
204
+ }
205
+ if (value.length <= 4) {
206
+ return "*".repeat(value.length);
207
+ }
208
+ return `${"*".repeat(value.length - 4)}${value.slice(-4)}`;
209
+ }
210
+ function validateResolvedConfig(resolution) {
211
+ const { values } = resolution;
212
+ const errors = [];
213
+ const warnings = [...resolution.warnings];
214
+ if (!values.baseUrl) {
215
+ errors.push("Missing required setting: JIRA_BASE_URL (flag/env/file).");
216
+ }
217
+ if (!values.projectKey && !values.issueKey) {
218
+ errors.push("Provide at least one context value: JIRA_PROJECT_KEY or JIRA_ISSUE_KEY.");
219
+ }
220
+ if (values.authMode === "api-token") {
221
+ if (!values.jiraEmail) {
222
+ errors.push("Auth mode api-token requires JIRA_EMAIL.");
223
+ }
224
+ if (!values.jiraApiToken) {
225
+ errors.push("Auth mode api-token requires JIRA_API_TOKEN.");
226
+ }
227
+ } else {
228
+ if (values.jiraApiToken || values.jiraEmail) {
229
+ warnings.push("JIRA_EMAIL/JIRA_API_TOKEN were provided but auth mode is 'none'.");
230
+ }
231
+ }
232
+ return { ok: errors.length === 0, errors, warnings };
233
+ }
234
+ function validateCliConfigResolution(resolution) {
235
+ return validateResolvedConfig(resolution);
236
+ }
237
+ function toDisplayLines(resolution) {
238
+ const { values, sources } = resolution;
239
+ return [
240
+ "Resolved config (secret-safe):",
241
+ ` baseUrl: ${values.baseUrl || "<unset>"} (${sources.baseUrl})`,
242
+ ` projectKey: ${values.projectKey || "<unset>"} (${sources.projectKey})`,
243
+ ` issueKey: ${values.issueKey || "<unset>"} (${sources.issueKey})`,
244
+ ` authMode: ${values.authMode} (${sources.authMode})`,
245
+ ` jiraEmail: ${values.jiraEmail || "<unset>"} (${sources.jiraEmail})`,
246
+ ` jiraApiToken: ${values.jiraApiToken ? maskSecret(values.jiraApiToken) : "<unset>"} (${sources.jiraApiToken})`,
247
+ ` configPath: ${values.configPath} (${sources.configPath})`
248
+ ];
249
+ }
250
+ function createConfigHandler(env = process.env, cwd = process.cwd()) {
251
+ return (request) => {
252
+ const [subcommand, ...subArgs] = request.args;
253
+ const resolution = resolveCliConfig(subArgs, env, cwd);
254
+ const validation = validateCliConfigResolution(resolution);
255
+ if (subcommand === "show") {
256
+ return {
257
+ exitCode: ExitCode.Success,
258
+ stdout: [...toDisplayLines(resolution), ...validation.warnings.map((line) => `WARN: ${line}`)]
259
+ };
260
+ }
261
+ if (subcommand === "validate") {
262
+ if (validation.ok) {
263
+ return {
264
+ exitCode: ExitCode.Success,
265
+ stdout: ["Config validation: PASS", ...toDisplayLines(resolution), ...validation.warnings.map((line) => `WARN: ${line}`)]
266
+ };
267
+ }
268
+ return {
269
+ exitCode: ExitCode.ValidationError,
270
+ stdout: ["Config validation: FAIL", ...toDisplayLines(resolution)],
271
+ stderr: validation.errors.map((line) => `ERROR: ${line}`)
272
+ };
273
+ }
274
+ return {
275
+ exitCode: ExitCode.UsageError,
276
+ stderr: [`Unsupported config subcommand: ${subcommand}`]
277
+ };
278
+ };
279
+ }
280
+
281
+ // src/autoDetect.ts
282
+ function toPosix(value) {
283
+ return value.replaceAll("\\", "/");
284
+ }
285
+ function endsWithOneOf(pathValue, suffixes) {
286
+ return suffixes.some((suffix) => pathValue.endsWith(suffix));
287
+ }
288
+ function countMatches(files, predicate, cap) {
289
+ let count = 0;
290
+ for (const filePath of files) {
291
+ if (!predicate(filePath)) {
292
+ continue;
293
+ }
294
+ count += 1;
295
+ if (count >= cap) {
296
+ return cap;
297
+ }
298
+ }
299
+ return count;
300
+ }
301
+ function confidenceForWinner(score) {
302
+ if (score <= 0) {
303
+ return "none";
304
+ }
305
+ if (score >= 10) {
306
+ return "high";
307
+ }
308
+ if (score >= 5) {
309
+ return "medium";
310
+ }
311
+ return "low";
312
+ }
313
+ function detectFramework(scannedFiles) {
314
+ const files = scannedFiles.map((filePath) => toPosix(filePath).toLowerCase());
315
+ const hints = {
316
+ playwright: [],
317
+ pytest: [],
318
+ bdd: []
319
+ };
320
+ const hasPlaywrightConfig = files.some(
321
+ (filePath) => endsWithOneOf(filePath, [
322
+ "playwright.config.ts",
323
+ "playwright.config.js",
324
+ "playwright.config.mjs",
325
+ "playwright.config.cjs"
326
+ ])
327
+ );
328
+ if (hasPlaywrightConfig) {
329
+ hints.playwright.push("playwright config");
330
+ }
331
+ const playwrightSpecCount = countMatches(
332
+ files,
333
+ (filePath) => /(^|\/)(tests?|e2e)\//.test(filePath) && (filePath.includes(".spec.") || filePath.includes(".test.")),
334
+ 3
335
+ );
336
+ if (playwrightSpecCount > 0) {
337
+ hints.playwright.push(`spec/test files x${playwrightSpecCount}`);
338
+ }
339
+ const hasPytestConfig = files.some(
340
+ (filePath) => endsWithOneOf(filePath, ["pytest.ini", "tox.ini"]) || filePath.endsWith("pyproject.toml")
341
+ );
342
+ if (hasPytestConfig) {
343
+ hints.pytest.push("pytest config");
344
+ }
345
+ const hasConftest = files.some((filePath) => filePath.endsWith("conftest.py"));
346
+ if (hasConftest) {
347
+ hints.pytest.push("conftest.py");
348
+ }
349
+ const pytestTestsCount = countMatches(
350
+ files,
351
+ (filePath) => filePath.endsWith(".py") && (/(^|\/)test_[^/]+\.py$/.test(filePath) || /(^|\/)[^/]+_test\.py$/.test(filePath)),
352
+ 4
353
+ );
354
+ if (pytestTestsCount > 0) {
355
+ hints.pytest.push(`python test files x${pytestTestsCount}`);
356
+ }
357
+ const featureFileCount = countMatches(files, (filePath) => filePath.endsWith(".feature"), 4);
358
+ if (featureFileCount > 0) {
359
+ hints.bdd.push(`feature files x${featureFileCount}`);
360
+ }
361
+ const hasBehaveConfig = files.some((filePath) => filePath.endsWith("behave.ini"));
362
+ if (hasBehaveConfig) {
363
+ hints.bdd.push("behave.ini");
364
+ }
365
+ const hasCucumberJson = files.some(
366
+ (filePath) => filePath.endsWith("cucumber.json") || filePath.endsWith("cucumber-report.json") || filePath.endsWith(".cucumber.json")
367
+ );
368
+ if (hasCucumberJson) {
369
+ hints.bdd.push("cucumber json");
370
+ }
371
+ const scores = {
372
+ playwright: 0,
373
+ pytest: 0,
374
+ bdd: 0
375
+ };
376
+ if (hasPlaywrightConfig) {
377
+ scores.playwright += 7;
378
+ }
379
+ scores.playwright += playwrightSpecCount;
380
+ if (hasPytestConfig) {
381
+ scores.pytest += 6;
382
+ }
383
+ if (hasConftest) {
384
+ scores.pytest += 2;
385
+ }
386
+ scores.pytest += pytestTestsCount;
387
+ scores.bdd += featureFileCount * 2;
388
+ if (hasBehaveConfig) {
389
+ scores.bdd += 4;
390
+ }
391
+ if (hasCucumberJson) {
392
+ scores.bdd += 3;
393
+ }
394
+ const candidates = Object.entries(scores).map(([framework, score]) => ({ framework, score })).filter((entry) => entry.score > 0).sort((a, b) => {
395
+ if (b.score !== a.score) {
396
+ return b.score - a.score;
397
+ }
398
+ return a.framework.localeCompare(b.framework);
399
+ });
400
+ if (candidates.length === 0) {
401
+ return {
402
+ mode: "unknown",
403
+ confidence: "none",
404
+ scores,
405
+ candidates: [],
406
+ hints
407
+ };
408
+ }
409
+ const topScore = candidates[0]?.score ?? 0;
410
+ const topCandidates = candidates.filter((candidate) => candidate.score === topScore);
411
+ const secondCandidate = candidates[1];
412
+ const nearTie = typeof secondCandidate?.score === "number" && topScore > 0 && topScore - secondCandidate.score <= 1;
413
+ if (topCandidates.length > 1 || nearTie) {
414
+ return {
415
+ mode: "ambiguous",
416
+ confidence: "low",
417
+ scores,
418
+ candidates,
419
+ hints
420
+ };
421
+ }
422
+ const winner = candidates[0]?.framework ?? "bdd";
423
+ return {
424
+ mode: winner,
425
+ confidence: confidenceForWinner(topScore),
426
+ scores,
427
+ candidates,
428
+ hints
429
+ };
430
+ }
431
+
432
+ // src/autoDiagnostics.ts
433
+ function toDiagnosticJsonValue(diagnostic) {
434
+ return {
435
+ code: diagnostic.code,
436
+ category: diagnostic.category,
437
+ level: diagnostic.level,
438
+ message: diagnostic.message,
439
+ suggestion: diagnostic.suggestion
440
+ };
441
+ }
442
+ function formatDiagnosticLine(diagnostic) {
443
+ return `[${diagnostic.code}] ${diagnostic.message} Suggestion: ${diagnostic.suggestion}`;
444
+ }
445
+ function sortDiagnostics(input) {
446
+ return [...input].sort((left, right) => {
447
+ if (left.level !== right.level) {
448
+ return left.level.localeCompare(right.level);
449
+ }
450
+ if (left.category !== right.category) {
451
+ return left.category.localeCompare(right.category);
452
+ }
453
+ if (left.code !== right.code) {
454
+ return left.code.localeCompare(right.code);
455
+ }
456
+ return left.message.localeCompare(right.message);
457
+ });
458
+ }
459
+ function diagnosticCatalog(input) {
460
+ const diagnostics = [];
461
+ if (!input.hasEndpoint) {
462
+ diagnostics.push({
463
+ code: "CONFIG_MISSING_ENDPOINT",
464
+ category: "config",
465
+ level: "warning",
466
+ message: "TESTOPS_FORGE_ENDPOINT is not configured.",
467
+ suggestion: "Set TESTOPS_FORGE_ENDPOINT to enable real upload mode."
468
+ });
469
+ }
470
+ if (!input.hasAuth) {
471
+ diagnostics.push({
472
+ code: "CONFIG_MISSING_AUTH",
473
+ category: "config",
474
+ level: "warning",
475
+ message: "TESTOPS_FORGE_AUTH_TOKEN is not configured.",
476
+ suggestion: "Set TESTOPS_FORGE_AUTH_TOKEN for authenticated upload mode."
477
+ });
478
+ }
479
+ if (input.artifactCount === 0) {
480
+ diagnostics.push({
481
+ code: "DISCOVERY_NO_ARTIFACTS",
482
+ category: "discovery",
483
+ level: "warning",
484
+ message: "No JUnit/Cucumber artifacts were discovered.",
485
+ suggestion: "Generate test result files or widen include patterns before execution."
486
+ });
487
+ }
488
+ if (input.detectionMode === "ambiguous") {
489
+ diagnostics.push({
490
+ code: "MAPPING_FRAMEWORK_AMBIGUOUS",
491
+ category: "mapping",
492
+ level: "warning",
493
+ message: "Multiple framework hints were detected with similar confidence.",
494
+ suggestion: "Use include/exclude options to narrow repository scope."
495
+ });
496
+ }
497
+ if (input.detectionMode === "unknown") {
498
+ diagnostics.push({
499
+ code: "MAPPING_FRAMEWORK_UNKNOWN",
500
+ category: "mapping",
501
+ level: "warning",
502
+ message: "No supported framework hints were detected.",
503
+ suggestion: "Verify repository layout or specify a narrower root path."
504
+ });
505
+ }
506
+ if (!input.dryRun) {
507
+ diagnostics.push({
508
+ code: "UPLOAD_EXECUTION_MODE_ACTIVE",
509
+ category: "upload",
510
+ level: "warning",
511
+ message: "Execution mode is active.",
512
+ suggestion: "Use --dry-run for preview-only validation if needed."
513
+ });
514
+ }
515
+ return sortDiagnostics(diagnostics);
516
+ }
517
+ function diagMappingBlocked() {
518
+ return {
519
+ code: "MAPPING_UNCERTAIN_BLOCKED",
520
+ category: "mapping",
521
+ level: "error",
522
+ message: "Execution blocked by uncertain mapping.",
523
+ suggestion: "Run --dry-run to inspect unmatched entities or pass --force to override."
524
+ };
525
+ }
526
+ function diagMissingProjectContext() {
527
+ return {
528
+ code: "CONFIG_MISSING_PROJECT_CONTEXT",
529
+ category: "config",
530
+ level: "error",
531
+ message: "Missing project context for execution mode.",
532
+ suggestion: "Set JIRA_PROJECT_KEY or pass --project-key."
533
+ };
534
+ }
535
+ function diagUploadFeatureFailure(featurePath, reason) {
536
+ return {
537
+ code: "UPLOAD_FEATURE_FAILED",
538
+ category: "upload",
539
+ level: "error",
540
+ message: `Feature upload failed for '${featurePath}': ${reason}`,
541
+ suggestion: "Check feature payload validity and Forge resolver logs."
542
+ };
543
+ }
544
+ function diagUploadRunFailure(source, scenario, reason) {
545
+ return {
546
+ code: "UPLOAD_RUN_FAILED",
547
+ category: "upload",
548
+ level: "error",
549
+ message: `Run upload failed for '${source}' scenario '${scenario}': ${reason}`,
550
+ suggestion: "Check run payload shape and retry after fixing failed steps."
551
+ };
552
+ }
553
+ function diagSkippedArtifact(pathValue, reason) {
554
+ return {
555
+ code: "DISCOVERY_ARTIFACT_SKIPPED",
556
+ category: "discovery",
557
+ level: "warning",
558
+ message: `Artifact skipped '${pathValue}': ${reason}`,
559
+ suggestion: "Use supported artifact formats or proceed with BDD/cucumber runs."
560
+ };
561
+ }
562
+ function diagExecutionNoWork() {
563
+ return {
564
+ code: "UPLOAD_NO_WORK_EXECUTED",
565
+ category: "upload",
566
+ level: "error",
567
+ message: "Execution completed without uploading any features or runs.",
568
+ suggestion: "Verify discovered features/artifacts and adjust include/exclude filters."
569
+ };
570
+ }
571
+
572
+ // src/autoDiscover.ts
573
+ import path2 from "node:path";
574
+ var RESULT_FOLDER_NAMES = /* @__PURE__ */ new Set([
575
+ "results",
576
+ "result",
577
+ "reports",
578
+ "report",
579
+ "test-results",
580
+ "allure-results",
581
+ "surefire-reports",
582
+ "failsafe-reports"
583
+ ]);
584
+ function toPosix2(value) {
585
+ return value.split(path2.sep).join("/");
586
+ }
587
+ function isJUnitXmlArtifact(filePath) {
588
+ const normalized = toPosix2(filePath);
589
+ const lower = normalized.toLowerCase();
590
+ if (!lower.endsWith(".xml")) {
591
+ return false;
592
+ }
593
+ const fileName = lower.split("/").at(-1) ?? "";
594
+ if (fileName.startsWith("junit") || fileName.startsWith("test-") || fileName.startsWith("testsuite")) {
595
+ return true;
596
+ }
597
+ return lower.includes("/surefire-reports/") || lower.includes("/failsafe-reports/") || lower.includes("/junit/") || lower.includes("/test-results/");
598
+ }
599
+ function isCucumberJsonArtifact(filePath) {
600
+ const normalized = toPosix2(filePath);
601
+ const lower = normalized.toLowerCase();
602
+ if (!lower.endsWith(".json")) {
603
+ return false;
604
+ }
605
+ const fileName = lower.split("/").at(-1) ?? "";
606
+ if (fileName === "cucumber.json" || fileName === "cucumber-report.json" || fileName.endsWith(".cucumber.json")) {
607
+ return true;
608
+ }
609
+ return lower.includes("/cucumber/") || lower.includes("/cucumber-reports/");
610
+ }
611
+ function extractResultFolders(files) {
612
+ const folderSet = /* @__PURE__ */ new Set();
613
+ for (const inputPath of files) {
614
+ const normalized = toPosix2(inputPath);
615
+ const segments = normalized.split("/");
616
+ if (segments.length < 2) {
617
+ continue;
618
+ }
619
+ for (let index = 0; index < segments.length - 1; index += 1) {
620
+ const segment = segments[index];
621
+ if (!RESULT_FOLDER_NAMES.has(segment.toLowerCase())) {
622
+ continue;
623
+ }
624
+ folderSet.add(segments.slice(0, index + 1).join("/"));
625
+ }
626
+ }
627
+ return [...folderSet].sort((a, b) => a.localeCompare(b));
628
+ }
629
+ function discoverArtifacts(scannedFiles) {
630
+ const discovered = [];
631
+ for (const filePath of scannedFiles) {
632
+ if (isJUnitXmlArtifact(filePath)) {
633
+ discovered.push({ kind: "junit-xml", path: filePath });
634
+ }
635
+ if (isCucumberJsonArtifact(filePath)) {
636
+ discovered.push({ kind: "cucumber-json", path: filePath });
637
+ }
638
+ }
639
+ const deduped = [...new Map(discovered.map((entry) => [`${entry.kind}:${entry.path}`, entry])).values()].sort(
640
+ (left, right) => {
641
+ if (left.path !== right.path) {
642
+ return left.path.localeCompare(right.path);
643
+ }
644
+ return left.kind.localeCompare(right.kind);
645
+ }
646
+ );
647
+ const junitXml = deduped.filter((entry) => entry.kind === "junit-xml").length;
648
+ const cucumberJson = deduped.filter((entry) => entry.kind === "cucumber-json").length;
649
+ const resultFolders = extractResultFolders(scannedFiles);
650
+ return {
651
+ artifacts: deduped,
652
+ resultFolders,
653
+ counts: {
654
+ junitXml,
655
+ cucumberJson,
656
+ total: deduped.length
657
+ }
658
+ };
659
+ }
660
+
661
+ // src/autoFeatureDiscovery.ts
662
+ function normalizePath(pathValue) {
663
+ return pathValue.replaceAll("\\", "/");
664
+ }
665
+ function discoverFeatureFiles(scannedFiles) {
666
+ const featurePaths = [...new Set(
667
+ scannedFiles.map((filePath) => normalizePath(filePath)).filter((filePath) => filePath.toLowerCase().endsWith(".feature"))
668
+ )].sort((a, b) => a.localeCompare(b));
669
+ return {
670
+ total: featurePaths.length,
671
+ paths: featurePaths,
672
+ sample: featurePaths.slice(0, 10)
673
+ };
674
+ }
675
+
676
+ // src/autoMapping.ts
677
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
678
+ import path3 from "node:path";
679
+ var ISSUE_KEY_REGEX = /\b[A-Z][A-Z0-9]+-\d+\b/g;
680
+ function normalizeName(value) {
681
+ return value.trim().toLowerCase();
682
+ }
683
+ function extractIssueKeysFromText(text) {
684
+ const found = text.match(ISSUE_KEY_REGEX) ?? [];
685
+ return [...new Set(found)].sort((a, b) => a.localeCompare(b));
686
+ }
687
+ function parseFeatureFile(featureAbsolutePath) {
688
+ if (!existsSync2(featureAbsolutePath)) {
689
+ return { names: [], issueKeys: [] };
690
+ }
691
+ const raw = readFileSync2(featureAbsolutePath, "utf8");
692
+ const lines = raw.split(/\r?\n/);
693
+ const names = /* @__PURE__ */ new Set();
694
+ const issueKeys = new Set(extractIssueKeysFromText(raw));
695
+ for (const line of lines) {
696
+ const trimmed = line.trim();
697
+ const featureMatch = /^feature:\s*(.+)$/i.exec(trimmed);
698
+ if (featureMatch?.[1]) {
699
+ names.add(featureMatch[1].trim());
700
+ }
701
+ const scenarioMatch = /^scenario(?: outline)?:\s*(.+)$/i.exec(trimmed);
702
+ if (scenarioMatch?.[1]) {
703
+ names.add(scenarioMatch[1].trim());
704
+ }
705
+ for (const issueKey of extractIssueKeysFromText(trimmed)) {
706
+ issueKeys.add(issueKey);
707
+ }
708
+ }
709
+ return {
710
+ names: [...names].sort((a, b) => a.localeCompare(b)),
711
+ issueKeys: [...issueKeys].sort((a, b) => a.localeCompare(b))
712
+ };
713
+ }
714
+ function collectRemoteSnapshot(input) {
715
+ if (!input) {
716
+ return { names: [], issueKeys: [] };
717
+ }
718
+ const names = /* @__PURE__ */ new Set();
719
+ const issueKeys = /* @__PURE__ */ new Set();
720
+ for (const testCase of input.testCases) {
721
+ names.add(testCase.key);
722
+ names.add(testCase.title);
723
+ for (const linkedIssueKey of testCase.linkedIssueKeys ?? []) {
724
+ issueKeys.add(linkedIssueKey);
725
+ }
726
+ }
727
+ for (const feature of input.bddArtifacts.features) {
728
+ names.add(feature.payload.name);
729
+ for (const linkedIssueKey of feature.linkedIssueKeys) {
730
+ issueKeys.add(linkedIssueKey);
731
+ }
732
+ }
733
+ for (const run of input.bddArtifacts.runs) {
734
+ names.add(run.payload.featureName);
735
+ names.add(run.payload.scenarioName);
736
+ for (const linkedIssueKey of run.linkedIssueKeys) {
737
+ issueKeys.add(linkedIssueKey);
738
+ }
739
+ }
740
+ return {
741
+ names: [...names].sort((a, b) => a.localeCompare(b)),
742
+ issueKeys: [...issueKeys].sort((a, b) => a.localeCompare(b))
743
+ };
744
+ }
745
+ function buildMappingPreview(input) {
746
+ const localNamesSet = /* @__PURE__ */ new Set();
747
+ const issueCandidatesSet = /* @__PURE__ */ new Set();
748
+ for (const featurePath of input.featurePaths) {
749
+ const absolutePath = path3.join(input.rootPath, featurePath);
750
+ const parsed = parseFeatureFile(absolutePath);
751
+ for (const name of parsed.names) {
752
+ localNamesSet.add(name);
753
+ }
754
+ for (const issueKey of parsed.issueKeys) {
755
+ issueCandidatesSet.add(issueKey);
756
+ }
757
+ }
758
+ const localNames = [...localNamesSet].sort((a, b) => a.localeCompare(b));
759
+ const issueKeyCandidates = [...issueCandidatesSet].sort((a, b) => a.localeCompare(b));
760
+ const remote = collectRemoteSnapshot(input.remoteSnapshot);
761
+ const remoteNameSet = new Set(remote.names.map((name) => normalizeName(name)));
762
+ const remoteIssueSet = new Set(remote.issueKeys);
763
+ if (input.contextIssueKey?.trim()) {
764
+ remoteIssueSet.add(input.contextIssueKey.trim());
765
+ }
766
+ const exactNameMatched = localNames.filter((name) => remoteNameSet.has(normalizeName(name))).sort((a, b) => a.localeCompare(b));
767
+ const unmatchedNames = localNames.filter((name) => !remoteNameSet.has(normalizeName(name))).sort((a, b) => a.localeCompare(b));
768
+ const issueKeyMatched = issueKeyCandidates.filter((issueKey) => remoteIssueSet.has(issueKey)).sort((a, b) => a.localeCompare(b));
769
+ const issueKeyUnmatched = issueKeyCandidates.filter((issueKey) => !remoteIssueSet.has(issueKey)).sort((a, b) => a.localeCompare(b));
770
+ const diagnostics = [];
771
+ if (localNames.length === 0) {
772
+ diagnostics.push("No feature/scenario names extracted for mapping preview.");
773
+ }
774
+ if (!input.remoteSnapshot) {
775
+ diagnostics.push("Remote snapshot unavailable; using estimate-only mapping.");
776
+ }
777
+ if (issueKeyCandidates.length === 0) {
778
+ diagnostics.push("No Jira issue-key candidates found in local feature text.");
779
+ }
780
+ if (unmatchedNames.length > 0) {
781
+ diagnostics.push(`Unmatched names: ${unmatchedNames.length}.`);
782
+ }
783
+ if (issueKeyUnmatched.length > 0) {
784
+ diagnostics.push(`Unmatched issue keys: ${issueKeyUnmatched.join(", ")}.`);
785
+ }
786
+ return {
787
+ mode: input.remoteSnapshot ? "contract-assisted" : "estimate-only",
788
+ issueKeyCandidates,
789
+ issueKeyMatched,
790
+ issueKeyUnmatched,
791
+ exactNameMatched,
792
+ unmatchedNames,
793
+ diagnostics
794
+ };
795
+ }
796
+
797
+ // src/autoScanner.ts
798
+ import { readdirSync } from "node:fs";
799
+ import path4 from "node:path";
800
+ var DEFAULT_MAX_DEPTH = 5;
801
+ var DEFAULT_EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set([
802
+ ".cache",
803
+ ".git",
804
+ ".next",
805
+ ".pnpm",
806
+ ".yarn",
807
+ "allure-report",
808
+ "build",
809
+ "coverage",
810
+ "dist",
811
+ "node_modules"
812
+ ]);
813
+ function normalizeRelativePath(value) {
814
+ return value.split(path4.sep).join("/");
815
+ }
816
+ function normalizePatterns(values) {
817
+ if (!values) {
818
+ return [];
819
+ }
820
+ return values.map((value) => normalizeRelativePath(value.trim())).filter(Boolean).sort((a, b) => a.localeCompare(b));
821
+ }
822
+ function safeMaxDepth(input) {
823
+ if (typeof input !== "number" || !Number.isFinite(input)) {
824
+ return DEFAULT_MAX_DEPTH;
825
+ }
826
+ if (input < 0) {
827
+ return 0;
828
+ }
829
+ return Math.floor(input);
830
+ }
831
+ function matchesAnyPattern(relativePath, patterns) {
832
+ const normalized = normalizeRelativePath(relativePath);
833
+ if (patterns.length === 0) {
834
+ return false;
835
+ }
836
+ return patterns.some((pattern) => {
837
+ if (normalized === pattern) {
838
+ return true;
839
+ }
840
+ if (normalized.startsWith(`${pattern}/`)) {
841
+ return true;
842
+ }
843
+ if (normalized.includes(pattern)) {
844
+ return true;
845
+ }
846
+ return false;
847
+ });
848
+ }
849
+ function isIncluded(relativePath, includePatterns) {
850
+ if (includePatterns.length === 0) {
851
+ return true;
852
+ }
853
+ return matchesAnyPattern(relativePath, includePatterns);
854
+ }
855
+ function isExcluded(relativePath, excludePatterns) {
856
+ return matchesAnyPattern(relativePath, excludePatterns);
857
+ }
858
+ function shouldSkipDefaultDirectory(directoryName, relativePath, includePatterns) {
859
+ if (!DEFAULT_EXCLUDED_DIRECTORIES.has(directoryName)) {
860
+ return false;
861
+ }
862
+ return !matchesAnyPattern(relativePath, includePatterns);
863
+ }
864
+ function scanProjectFiles(options) {
865
+ const includePatterns = normalizePatterns(options.includePatterns);
866
+ const excludePatterns = normalizePatterns(options.excludePatterns);
867
+ const maxDepth = safeMaxDepth(options.maxDepth);
868
+ const rootPath = path4.resolve(options.rootPath);
869
+ const scannedFiles = [];
870
+ const visit = (absoluteDirectory, depth) => {
871
+ if (depth > maxDepth) {
872
+ return;
873
+ }
874
+ const entries = readdirSync(absoluteDirectory, { withFileTypes: true }).slice().sort((a, b) => a.name.localeCompare(b.name));
875
+ for (const entry of entries) {
876
+ const absoluteEntryPath = path4.join(absoluteDirectory, entry.name);
877
+ const relativeEntryPath = normalizeRelativePath(path4.relative(rootPath, absoluteEntryPath));
878
+ if (!relativeEntryPath) {
879
+ continue;
880
+ }
881
+ if (isExcluded(relativeEntryPath, excludePatterns)) {
882
+ continue;
883
+ }
884
+ if (entry.isDirectory()) {
885
+ if (shouldSkipDefaultDirectory(entry.name, relativeEntryPath, includePatterns)) {
886
+ continue;
887
+ }
888
+ visit(absoluteEntryPath, depth + 1);
889
+ continue;
890
+ }
891
+ if (!isIncluded(relativeEntryPath, includePatterns)) {
892
+ continue;
893
+ }
894
+ scannedFiles.push(relativeEntryPath);
895
+ }
896
+ };
897
+ visit(rootPath, 0);
898
+ scannedFiles.sort((a, b) => a.localeCompare(b));
899
+ return {
900
+ rootPath,
901
+ maxDepth,
902
+ scannedFiles
903
+ };
904
+ }
905
+
906
+ // src/auto.ts
907
+ var DETERMINISTIC_STEPS = [
908
+ "detect",
909
+ "discover",
910
+ "map-preview",
911
+ "upload"
912
+ ];
913
+ function parseArgs(args) {
914
+ const flags = {};
915
+ const boolFlags = /* @__PURE__ */ new Set();
916
+ const unknownFlags = [];
917
+ const valuedFlags = /* @__PURE__ */ new Set([
918
+ "--root",
919
+ "--include",
920
+ "--exclude",
921
+ "--output",
922
+ "--max-depth",
923
+ "--config",
924
+ "--base-url",
925
+ "--project-key",
926
+ "--issue-key",
927
+ "--auth-mode",
928
+ "--jira-email",
929
+ "--jira-api-token"
930
+ ]);
931
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--dry-run", "--json", "--force"]);
932
+ for (let i = 0; i < args.length; i += 1) {
933
+ const token = args[i];
934
+ if (!token.startsWith("--")) {
935
+ continue;
936
+ }
937
+ if (supportedBoolFlags.has(token)) {
938
+ boolFlags.add(token);
939
+ continue;
940
+ }
941
+ if (!valuedFlags.has(token)) {
942
+ unknownFlags.push(token);
943
+ continue;
944
+ }
945
+ const value = args[i + 1];
946
+ if (!value || value.startsWith("--")) {
947
+ unknownFlags.push(token);
948
+ continue;
949
+ }
950
+ flags[token] = value;
951
+ i += 1;
952
+ }
953
+ return { flags, boolFlags, unknownFlags };
954
+ }
955
+ var CONFIG_FLAGS = /* @__PURE__ */ new Set([
956
+ "--config",
957
+ "--base-url",
958
+ "--project-key",
959
+ "--issue-key",
960
+ "--auth-mode",
961
+ "--jira-email",
962
+ "--jira-api-token"
963
+ ]);
964
+ function pickConfigArgs(parsed) {
965
+ const configArgs = [];
966
+ for (const [flag, value] of Object.entries(parsed.flags)) {
967
+ if (!CONFIG_FLAGS.has(flag)) {
968
+ continue;
969
+ }
970
+ configArgs.push(flag, value);
971
+ }
972
+ return configArgs;
973
+ }
974
+ function normalizeListFlag(value) {
975
+ if (!value.trim()) {
976
+ return [];
977
+ }
978
+ return value.split(",").map((part) => part.trim()).filter(Boolean).sort((a, b) => a.localeCompare(b));
979
+ }
980
+ function resolveRoot(rootFlag, cwd) {
981
+ if (!rootFlag.trim()) {
982
+ return cwd;
983
+ }
984
+ return path5.resolve(cwd, rootFlag);
985
+ }
986
+ function countScenariosInFeatureFile(featurePath) {
987
+ const lines = readFileSync3(featurePath, "utf8").split(/\r?\n/).map((line) => line.trim().toLowerCase());
988
+ let count = 0;
989
+ for (const line of lines) {
990
+ if (line.startsWith("scenario:") || line.startsWith("scenario outline:")) {
991
+ count += 1;
992
+ }
993
+ }
994
+ return count;
995
+ }
996
+ function buildScenarioStats(rootPath, featurePaths) {
997
+ let scenarios = 0;
998
+ for (const relativePath of featurePaths) {
999
+ const absolutePath = path5.join(rootPath, relativePath);
1000
+ if (!existsSync3(absolutePath)) {
1001
+ continue;
1002
+ }
1003
+ scenarios += countScenariosInFeatureFile(absolutePath);
1004
+ }
1005
+ return { scenarios };
1006
+ }
1007
+ function toPrefixBeforeFeatures(featurePath) {
1008
+ const normalized = featurePath.replaceAll("\\", "/");
1009
+ const marker = "/features/";
1010
+ const index = normalized.indexOf(marker);
1011
+ if (index <= 0) {
1012
+ return "";
1013
+ }
1014
+ return normalized.slice(0, index);
1015
+ }
1016
+ function deriveFeaturePairingHints(featurePaths, artifactPaths) {
1017
+ const hints = /* @__PURE__ */ new Set();
1018
+ for (const featurePath of featurePaths) {
1019
+ const featurePrefix = toPrefixBeforeFeatures(featurePath);
1020
+ if (!featurePrefix) {
1021
+ continue;
1022
+ }
1023
+ for (const artifactPath of artifactPaths) {
1024
+ if (!artifactPath.startsWith(`${featurePrefix}/`)) {
1025
+ continue;
1026
+ }
1027
+ hints.add(`${featurePath} -> ${artifactPath}`);
1028
+ }
1029
+ }
1030
+ return [...hints].sort((a, b) => a.localeCompare(b));
1031
+ }
1032
+ function deriveFeatureNameFromGherkin(gherkin, fallbackPath) {
1033
+ const lines = gherkin.split(/\r?\n/);
1034
+ for (const line of lines) {
1035
+ const match = /^feature:\s*(.+)$/i.exec(line.trim());
1036
+ if (match?.[1]?.trim()) {
1037
+ return match[1].trim();
1038
+ }
1039
+ }
1040
+ return path5.basename(fallbackPath, path5.extname(fallbackPath));
1041
+ }
1042
+ function mapStepStatus(status) {
1043
+ const normalized = status.trim().toLowerCase();
1044
+ if (normalized === "passed" || normalized === "pass") {
1045
+ return StepResult.Passed;
1046
+ }
1047
+ if (normalized === "failed" || normalized === "fail") {
1048
+ return StepResult.Failed;
1049
+ }
1050
+ if (normalized === "skipped" || normalized === "pending" || normalized === "undefined") {
1051
+ return StepResult.Skipped;
1052
+ }
1053
+ return StepResult.Blocked;
1054
+ }
1055
+ function parseCucumberRunsFromArtifact(rootPath, artifactPath) {
1056
+ const absolutePath = path5.join(rootPath, artifactPath);
1057
+ if (!existsSync3(absolutePath)) {
1058
+ return [];
1059
+ }
1060
+ let parsed;
1061
+ try {
1062
+ parsed = JSON.parse(readFileSync3(absolutePath, "utf8"));
1063
+ } catch {
1064
+ return [];
1065
+ }
1066
+ if (!Array.isArray(parsed)) {
1067
+ return [];
1068
+ }
1069
+ const candidates = [];
1070
+ for (const featureEntry of parsed) {
1071
+ if (!featureEntry || typeof featureEntry !== "object") {
1072
+ continue;
1073
+ }
1074
+ const feature = featureEntry;
1075
+ const featureName = typeof feature.name === "string" ? feature.name.trim() : "";
1076
+ const elements = Array.isArray(feature.elements) ? feature.elements : Array.isArray(feature.scenarios) ? feature.scenarios : [];
1077
+ for (const elementEntry of elements) {
1078
+ if (!elementEntry || typeof elementEntry !== "object") {
1079
+ continue;
1080
+ }
1081
+ const element = elementEntry;
1082
+ const scenarioName = typeof element.name === "string" ? element.name.trim() : "";
1083
+ const stepEntries = Array.isArray(element.steps) ? element.steps : [];
1084
+ const steps = [];
1085
+ for (const stepEntry of stepEntries) {
1086
+ if (!stepEntry || typeof stepEntry !== "object") {
1087
+ continue;
1088
+ }
1089
+ const step = stepEntry;
1090
+ const stepText = `${typeof step.keyword === "string" ? step.keyword.trim() : ""} ${typeof step.name === "string" ? step.name.trim() : ""}`.trim();
1091
+ if (!stepText) {
1092
+ continue;
1093
+ }
1094
+ const result = step.result && typeof step.result === "object" ? step.result : {};
1095
+ const mapped = mapStepStatus(typeof result.status === "string" ? result.status : "");
1096
+ const errorMessage = typeof result.error_message === "string" ? result.error_message : "";
1097
+ steps.push({
1098
+ step: stepText,
1099
+ result: mapped,
1100
+ error: mapped === StepResult.Failed ? errorMessage || "cucumber step failed" : void 0
1101
+ });
1102
+ }
1103
+ if (!featureName || !scenarioName || steps.length === 0) {
1104
+ continue;
1105
+ }
1106
+ candidates.push({
1107
+ sourcePath: artifactPath,
1108
+ input: {
1109
+ featureName,
1110
+ scenarioName,
1111
+ executedAt: (/* @__PURE__ */ new Date()).toISOString(),
1112
+ steps
1113
+ }
1114
+ });
1115
+ }
1116
+ }
1117
+ return candidates.sort((left, right) => {
1118
+ if (left.sourcePath !== right.sourcePath) {
1119
+ return left.sourcePath.localeCompare(right.sourcePath);
1120
+ }
1121
+ if (left.input.featureName !== right.input.featureName) {
1122
+ return left.input.featureName.localeCompare(right.input.featureName);
1123
+ }
1124
+ return left.input.scenarioName.localeCompare(right.input.scenarioName);
1125
+ });
1126
+ }
1127
+ function normalizeExecutionError(error) {
1128
+ if (error instanceof Error) {
1129
+ return error.message;
1130
+ }
1131
+ return "unknown execution error";
1132
+ }
1133
+ async function executeUploadFlow(input) {
1134
+ if ((input.mappingPreview.issueKeyUnmatched.length > 0 || input.mappingPreview.unmatchedNames.length > 0) && !input.force) {
1135
+ const blockingDiagnostic = diagMappingBlocked();
1136
+ return {
1137
+ executed: false,
1138
+ blockedReason: blockingDiagnostic.message,
1139
+ featureSuccess: 0,
1140
+ featureFailed: 0,
1141
+ runSuccess: 0,
1142
+ runFailed: 0,
1143
+ skippedArtifacts: [],
1144
+ errors: [],
1145
+ diagnostics: [blockingDiagnostic]
1146
+ };
1147
+ }
1148
+ let featureSuccess = 0;
1149
+ let featureFailed = 0;
1150
+ let runSuccess = 0;
1151
+ let runFailed = 0;
1152
+ const skippedArtifacts = [];
1153
+ const errors = [];
1154
+ const diagnostics = [];
1155
+ const orderedFeatures = [...input.featurePaths].sort((a, b) => a.localeCompare(b));
1156
+ for (const featurePath of orderedFeatures) {
1157
+ const absolutePath = path5.join(input.rootPath, featurePath);
1158
+ if (!existsSync3(absolutePath)) {
1159
+ featureFailed += 1;
1160
+ const diagnostic = diagUploadFeatureFailure(featurePath, "missing feature file");
1161
+ errors.push(diagnostic.message);
1162
+ diagnostics.push(diagnostic);
1163
+ continue;
1164
+ }
1165
+ const gherkin = readFileSync3(absolutePath, "utf8");
1166
+ const name = deriveFeatureNameFromGherkin(gherkin, featurePath);
1167
+ const payload = {
1168
+ context: {
1169
+ projectKey: input.projectKey,
1170
+ issueKey: input.issueKey || void 0
1171
+ },
1172
+ input: {
1173
+ name,
1174
+ gherkin
1175
+ }
1176
+ };
1177
+ try {
1178
+ const response = await input.context.invokeForgeContract("ingestBddFeature", payload);
1179
+ if (response.ok) {
1180
+ featureSuccess += 1;
1181
+ } else {
1182
+ featureFailed += 1;
1183
+ const diagnostic = diagUploadFeatureFailure(featurePath, response.error.code);
1184
+ errors.push(diagnostic.message);
1185
+ diagnostics.push(diagnostic);
1186
+ }
1187
+ } catch (error) {
1188
+ featureFailed += 1;
1189
+ const diagnostic = diagUploadFeatureFailure(featurePath, normalizeExecutionError(error));
1190
+ errors.push(diagnostic.message);
1191
+ diagnostics.push(diagnostic);
1192
+ }
1193
+ }
1194
+ const orderedArtifacts = [...input.artifactList].sort((a, b) => a.path.localeCompare(b.path));
1195
+ for (const artifact of orderedArtifacts) {
1196
+ if (artifact.kind === "cucumber-json") {
1197
+ const runs = parseCucumberRunsFromArtifact(input.rootPath, artifact.path);
1198
+ if (runs.length === 0) {
1199
+ const diagnostic2 = diagSkippedArtifact(artifact.path, "no runnable scenarios");
1200
+ skippedArtifacts.push(diagnostic2.message);
1201
+ diagnostics.push(diagnostic2);
1202
+ continue;
1203
+ }
1204
+ for (const run of runs) {
1205
+ const payload = {
1206
+ context: {
1207
+ projectKey: input.projectKey,
1208
+ issueKey: input.issueKey || void 0
1209
+ },
1210
+ input: run.input
1211
+ };
1212
+ try {
1213
+ const response = await input.context.invokeForgeContract("ingestBddRun", payload);
1214
+ if (response.ok) {
1215
+ runSuccess += 1;
1216
+ } else {
1217
+ runFailed += 1;
1218
+ const diagnostic2 = diagUploadRunFailure(run.sourcePath, run.input.scenarioName, response.error.code);
1219
+ errors.push(diagnostic2.message);
1220
+ diagnostics.push(diagnostic2);
1221
+ }
1222
+ } catch (error) {
1223
+ runFailed += 1;
1224
+ const diagnostic2 = diagUploadRunFailure(
1225
+ run.sourcePath,
1226
+ run.input.scenarioName,
1227
+ normalizeExecutionError(error)
1228
+ );
1229
+ errors.push(diagnostic2.message);
1230
+ diagnostics.push(diagnostic2);
1231
+ }
1232
+ }
1233
+ continue;
1234
+ }
1235
+ if (artifact.kind === "junit-xml") {
1236
+ const diagnostic2 = diagSkippedArtifact(artifact.path, "junit upload not supported in AUTO-8");
1237
+ skippedArtifacts.push(diagnostic2.message);
1238
+ diagnostics.push(diagnostic2);
1239
+ continue;
1240
+ }
1241
+ const diagnostic = diagSkippedArtifact(artifact.path, `unsupported artifact kind: ${artifact.kind}`);
1242
+ skippedArtifacts.push(diagnostic.message);
1243
+ diagnostics.push(diagnostic);
1244
+ }
1245
+ if (featureSuccess + runSuccess === 0) {
1246
+ diagnostics.push(diagExecutionNoWork());
1247
+ }
1248
+ return {
1249
+ executed: true,
1250
+ featureSuccess,
1251
+ featureFailed,
1252
+ runSuccess,
1253
+ runFailed,
1254
+ skippedArtifacts: skippedArtifacts.sort((a, b) => a.localeCompare(b)),
1255
+ errors: errors.sort((a, b) => a.localeCompare(b)),
1256
+ diagnostics: sortDiagnostics(diagnostics)
1257
+ };
1258
+ }
1259
+ function buildSummary(input) {
1260
+ const scanSample = input.scannedFiles.slice(0, 10);
1261
+ const detection = detectFramework(input.scannedFiles);
1262
+ const discovery = discoverArtifacts(input.scannedFiles);
1263
+ const featureDiscovery = discoverFeatureFiles(input.scannedFiles);
1264
+ const scenarioStats = buildScenarioStats(input.rootPath, featureDiscovery.paths);
1265
+ const candidateSummary = detection.candidates.map((candidate) => ({
1266
+ framework: candidate.framework,
1267
+ score: candidate.score
1268
+ }));
1269
+ const artifactSummary = discovery.artifacts.map((artifact) => ({
1270
+ kind: artifact.kind,
1271
+ path: artifact.path
1272
+ }));
1273
+ const featurePairingHints = deriveFeaturePairingHints(
1274
+ featureDiscovery.paths,
1275
+ artifactSummary.map((artifact) => artifact.path)
1276
+ );
1277
+ const matchSummary = {
1278
+ mode: input.mappingPreview.mode,
1279
+ matched: input.mappingPreview.exactNameMatched.length,
1280
+ unmatched: input.mappingPreview.unmatchedNames.length,
1281
+ issueKeyCandidates: input.mappingPreview.issueKeyCandidates.length,
1282
+ issueKeyMatched: input.mappingPreview.issueKeyMatched.length,
1283
+ issueKeyUnmatched: input.mappingPreview.issueKeyUnmatched.length,
1284
+ note: "Deterministic MVP mapping preview."
1285
+ };
1286
+ const warnings = diagnosticCatalog({
1287
+ dryRun: input.dryRun,
1288
+ detectionMode: detection.mode,
1289
+ artifactCount: discovery.counts.total,
1290
+ hasEndpoint: Boolean(input.env.TESTOPS_FORGE_ENDPOINT?.trim()),
1291
+ hasAuth: Boolean(input.env.TESTOPS_FORGE_AUTH_TOKEN?.trim())
1292
+ });
1293
+ return {
1294
+ action: "auto",
1295
+ mode: input.dryRun ? "dry-run" : "execute",
1296
+ root: input.rootPath,
1297
+ include: input.include,
1298
+ exclude: input.exclude,
1299
+ output: input.output,
1300
+ deterministicOrder: DETERMINISTIC_STEPS,
1301
+ scan: {
1302
+ maxDepth: input.maxDepth,
1303
+ fileCount: input.scannedFiles.length,
1304
+ sample: scanSample
1305
+ },
1306
+ detection: {
1307
+ mode: detection.mode,
1308
+ confidence: detection.confidence,
1309
+ scores: detection.scores,
1310
+ candidates: candidateSummary,
1311
+ hints: detection.hints
1312
+ },
1313
+ discovery: {
1314
+ counts: discovery.counts,
1315
+ artifacts: artifactSummary,
1316
+ resultFolders: discovery.resultFolders,
1317
+ bddFeatures: {
1318
+ total: featureDiscovery.total,
1319
+ sample: featureDiscovery.sample,
1320
+ paths: featureDiscovery.paths,
1321
+ pairingHints: featurePairingHints
1322
+ }
1323
+ },
1324
+ preview: {
1325
+ counts: {
1326
+ testsEstimate: discovery.counts.total,
1327
+ scenarios: scenarioStats.scenarios
1328
+ },
1329
+ matchSummary,
1330
+ mapping: {
1331
+ issueKeyCandidates: input.mappingPreview.issueKeyCandidates,
1332
+ issueKeyMatched: input.mappingPreview.issueKeyMatched,
1333
+ issueKeyUnmatched: input.mappingPreview.issueKeyUnmatched,
1334
+ exactNameMatched: input.mappingPreview.exactNameMatched,
1335
+ unmatchedNames: input.mappingPreview.unmatchedNames,
1336
+ diagnostics: input.mappingPreview.diagnostics
1337
+ },
1338
+ warnings: warnings.map((warning) => toDiagnosticJsonValue(warning))
1339
+ },
1340
+ execution: input.execution ? {
1341
+ executed: input.execution.executed,
1342
+ blockedReason: input.execution.blockedReason ?? "",
1343
+ featureSuccess: input.execution.featureSuccess,
1344
+ featureFailed: input.execution.featureFailed,
1345
+ runSuccess: input.execution.runSuccess,
1346
+ runFailed: input.execution.runFailed,
1347
+ skippedArtifacts: input.execution.skippedArtifacts,
1348
+ errors: input.execution.errors,
1349
+ diagnostics: input.execution.diagnostics.map((diagnostic) => toDiagnosticJsonValue(diagnostic))
1350
+ } : {
1351
+ executed: false,
1352
+ blockedReason: "",
1353
+ featureSuccess: 0,
1354
+ featureFailed: 0,
1355
+ runSuccess: 0,
1356
+ runFailed: 0,
1357
+ skippedArtifacts: [],
1358
+ errors: [],
1359
+ diagnostics: []
1360
+ }
1361
+ };
1362
+ }
1363
+ function toHumanLines(summary) {
1364
+ const mode = String(summary.mode ?? "dry-run");
1365
+ const root = String(summary.root ?? "");
1366
+ const include = summary.include ?? [];
1367
+ const exclude = summary.exclude ?? [];
1368
+ const output = String(summary.output ?? "plain");
1369
+ const order = summary.deterministicOrder ?? [];
1370
+ const scan = summary.scan ?? {};
1371
+ const detection = summary.detection ?? {};
1372
+ const discovery = summary.discovery ?? {};
1373
+ const preview = summary.preview ?? {};
1374
+ const execution = summary.execution ?? {};
1375
+ const scanCount = typeof scan.fileCount === "number" ? scan.fileCount : 0;
1376
+ const scanDepth = typeof scan.maxDepth === "number" ? scan.maxDepth : 0;
1377
+ const scanSample = (scan.sample ?? []).slice(0, 5);
1378
+ const detectionMode = String(detection.mode ?? "unknown");
1379
+ const detectionConfidence = String(detection.confidence ?? "none");
1380
+ const topCandidates = (detection.candidates ?? []).slice(0, 3).map((candidate) => `${candidate.framework ?? "unknown"}:${candidate.score ?? 0}`);
1381
+ const discoveryCounts = discovery.counts ?? {};
1382
+ const artifactTotal = typeof discoveryCounts.total === "number" ? discoveryCounts.total : 0;
1383
+ const junitTotal = typeof discoveryCounts.junitXml === "number" ? discoveryCounts.junitXml : 0;
1384
+ const cucumberTotal = typeof discoveryCounts.cucumberJson === "number" ? discoveryCounts.cucumberJson : 0;
1385
+ const artifactSample = (discovery.artifacts ?? []).slice(0, 5).map((artifact) => `${artifact.kind ?? "unknown"}:${artifact.path ?? ""}`);
1386
+ const resultFolderSample = (discovery.resultFolders ?? []).slice(0, 5);
1387
+ const bddFeatures = discovery.bddFeatures ?? {};
1388
+ const bddFeatureTotal = typeof bddFeatures.total === "number" ? bddFeatures.total : 0;
1389
+ const bddFeatureSample = (bddFeatures.sample ?? []).slice(0, 5);
1390
+ const pairingHints = (bddFeatures.pairingHints ?? []).slice(0, 3);
1391
+ const previewCounts = preview.counts ?? {};
1392
+ const testsEstimate = typeof previewCounts.testsEstimate === "number" ? previewCounts.testsEstimate : 0;
1393
+ const scenarios = typeof previewCounts.scenarios === "number" ? previewCounts.scenarios : 0;
1394
+ const matchSummary = preview.matchSummary ?? {};
1395
+ const matched = typeof matchSummary.matched === "number" ? matchSummary.matched : 0;
1396
+ const unmatched = typeof matchSummary.unmatched === "number" ? matchSummary.unmatched : 0;
1397
+ const issueKeyCandidates = typeof matchSummary.issueKeyCandidates === "number" ? matchSummary.issueKeyCandidates : 0;
1398
+ const issueKeyMatched = typeof matchSummary.issueKeyMatched === "number" ? matchSummary.issueKeyMatched : 0;
1399
+ const issueKeyUnmatched = typeof matchSummary.issueKeyUnmatched === "number" ? matchSummary.issueKeyUnmatched : 0;
1400
+ const matchMode = String(matchSummary.mode ?? "estimate-only");
1401
+ const mapping = preview.mapping ?? {};
1402
+ const unmatchedNameSample = (mapping.unmatchedNames ?? []).slice(0, 3);
1403
+ const diagnosticSample = (mapping.diagnostics ?? []).slice(0, 3);
1404
+ const warningLines = (preview.warnings ?? []).slice(0, 5).map((warning) => {
1405
+ return formatDiagnosticLine({
1406
+ code: warning.code ?? "WARN",
1407
+ category: warning.category ?? "contract",
1408
+ level: warning.level ?? "warning",
1409
+ message: warning.message ?? "",
1410
+ suggestion: warning.suggestion ?? ""
1411
+ });
1412
+ });
1413
+ const executionDiagnosticLines = (execution.diagnostics ?? []).slice(0, 5).map(
1414
+ (diagnostic) => formatDiagnosticLine({
1415
+ code: diagnostic.code ?? "EXEC",
1416
+ category: diagnostic.category ?? "upload",
1417
+ level: diagnostic.level ?? "error",
1418
+ message: diagnostic.message ?? "",
1419
+ suggestion: diagnostic.suggestion ?? ""
1420
+ })
1421
+ );
1422
+ const executionBlockedReason = String(execution.blockedReason ?? "");
1423
+ const executionSkipped = (execution.skippedArtifacts ?? []).slice(0, 3);
1424
+ const executionErrors = (execution.errors ?? []).slice(0, 3);
1425
+ const executionFeatureSuccess = typeof execution.featureSuccess === "number" ? execution.featureSuccess : 0;
1426
+ const executionFeatureFailed = typeof execution.featureFailed === "number" ? execution.featureFailed : 0;
1427
+ const executionRunSuccess = typeof execution.runSuccess === "number" ? execution.runSuccess : 0;
1428
+ const executionRunFailed = typeof execution.runFailed === "number" ? execution.runFailed : 0;
1429
+ const executionAttempted = Boolean(execution.executed);
1430
+ return [
1431
+ `Auto mode: ${mode.toUpperCase()}`,
1432
+ `Root: ${root}`,
1433
+ `Include: ${include.length > 0 ? include.join(", ") : "<none>"}`,
1434
+ `Exclude: ${exclude.length > 0 ? exclude.join(", ") : "<none>"}`,
1435
+ `Max depth: ${scanDepth}`,
1436
+ `Output: ${output}`,
1437
+ `Scanned files: ${scanCount}`,
1438
+ `Scanned sample: ${scanSample.length > 0 ? scanSample.join(", ") : "<none>"}`,
1439
+ `Framework detection: ${detectionMode} (confidence: ${detectionConfidence})`,
1440
+ `Detection candidates: ${topCandidates.length > 0 ? topCandidates.join(", ") : "<none>"}`,
1441
+ `Discovered artifacts: ${artifactTotal} (junit=${junitTotal}, cucumber=${cucumberTotal})`,
1442
+ `Artifact sample: ${artifactSample.length > 0 ? artifactSample.join(", ") : "<none>"}`,
1443
+ `Result folders: ${resultFolderSample.length > 0 ? resultFolderSample.join(", ") : "<none>"}`,
1444
+ `BDD feature files: ${bddFeatureTotal}`,
1445
+ `BDD feature sample: ${bddFeatureSample.length > 0 ? bddFeatureSample.join(", ") : "<none>"}`,
1446
+ `BDD pairing hints: ${pairingHints.length > 0 ? pairingHints.join(", ") : "<none>"}`,
1447
+ `Preview counts: tests=${testsEstimate}, scenarios=${scenarios}`,
1448
+ `Match summary (${matchMode}): matched=${matched}, unmatched=${unmatched}`,
1449
+ `Issue-key summary: candidates=${issueKeyCandidates}, matched=${issueKeyMatched}, unmatched=${issueKeyUnmatched}`,
1450
+ `Unmatched names sample: ${unmatchedNameSample.length > 0 ? unmatchedNameSample.join(", ") : "<none>"}`,
1451
+ `Mapping diagnostics: ${diagnosticSample.length > 0 ? diagnosticSample.join(" | ") : "<none>"}`,
1452
+ `Warnings: ${warningLines.length > 0 ? warningLines.join("; ") : "<none>"}`,
1453
+ `Execution attempted: ${executionAttempted ? "yes" : "no"}`,
1454
+ `Execution summary: features ok=${executionFeatureSuccess}, features failed=${executionFeatureFailed}, runs ok=${executionRunSuccess}, runs failed=${executionRunFailed}`,
1455
+ `Execution skipped artifacts: ${executionSkipped.length > 0 ? executionSkipped.join(", ") : "<none>"}`,
1456
+ `Execution errors: ${executionErrors.length > 0 ? executionErrors.join(" | ") : executionBlockedReason || "<none>"}`,
1457
+ `Execution diagnostics: ${executionDiagnosticLines.length > 0 ? executionDiagnosticLines.join(" | ") : "<none>"}`,
1458
+ `Deterministic flow: ${order.join(" -> ")}`,
1459
+ mode === "dry-run" ? "Dry-run only: no uploads or mutations are performed." : "Execution mode contract only: upload stage is delegated to Forge wrappers."
1460
+ ];
1461
+ }
1462
+ function buildAutoContractHelp() {
1463
+ return [
1464
+ "auto command",
1465
+ "",
1466
+ "Usage:",
1467
+ " testops auto [--dry-run] [--force] [--root <path>] [--include <csv>] [--exclude <csv>] [--max-depth <n>] [--output plain|json]",
1468
+ "",
1469
+ "Deterministic execution order:",
1470
+ " detect -> discover -> map-preview -> upload",
1471
+ " detection output includes confidence and explicit ambiguous/unknown states",
1472
+ " dry-run preview includes counts, match estimate, warnings, and suggestions",
1473
+ "",
1474
+ "Thin-client boundary:",
1475
+ " CLI orchestrates only and must delegate domain decisions/uploads to Forge contracts."
1476
+ ].join("\n");
1477
+ }
1478
+ function createAutoHandler(deps = {}) {
1479
+ const cwd = deps.cwd ?? process.cwd();
1480
+ const env = deps.env ?? process.env;
1481
+ return async (request, context) => {
1482
+ const parsed = parseArgs(request.args);
1483
+ const dryRun = parsed.boolFlags.has("--dry-run");
1484
+ const force = parsed.boolFlags.has("--force");
1485
+ const useJson = parsed.boolFlags.has("--json") || parsed.flags["--output"] === "json";
1486
+ const output = parsed.flags["--output"]?.trim() || (useJson ? "json" : "plain");
1487
+ const rootPath = resolveRoot(parsed.flags["--root"] ?? "", cwd);
1488
+ const maxDepthValue = parsed.flags["--max-depth"]?.trim();
1489
+ const include = normalizeListFlag(parsed.flags["--include"] ?? "");
1490
+ const exclude = normalizeListFlag(parsed.flags["--exclude"] ?? "");
1491
+ const configResolution = resolveCliConfig(pickConfigArgs(parsed), env, cwd);
1492
+ if (parsed.unknownFlags.length > 0) {
1493
+ return {
1494
+ exitCode: ExitCode.UsageError,
1495
+ stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
1496
+ };
1497
+ }
1498
+ if (output !== "plain" && output !== "json") {
1499
+ return {
1500
+ exitCode: ExitCode.UsageError,
1501
+ stderr: ["ERROR: --output must be either 'plain' or 'json'."]
1502
+ };
1503
+ }
1504
+ if (!existsSync3(rootPath)) {
1505
+ return {
1506
+ exitCode: ExitCode.ValidationError,
1507
+ stderr: [`ERROR: Root path does not exist: ${rootPath}`]
1508
+ };
1509
+ }
1510
+ if (maxDepthValue && !/^\d+$/.test(maxDepthValue)) {
1511
+ return {
1512
+ exitCode: ExitCode.UsageError,
1513
+ stderr: ["ERROR: --max-depth must be a non-negative integer."]
1514
+ };
1515
+ }
1516
+ const maxDepth = maxDepthValue ? Number.parseInt(maxDepthValue, 10) : 5;
1517
+ const scanResult = scanProjectFiles({
1518
+ rootPath,
1519
+ includePatterns: include,
1520
+ excludePatterns: exclude,
1521
+ maxDepth
1522
+ });
1523
+ let remoteSnapshot;
1524
+ const projectKey = configResolution.values.projectKey;
1525
+ const issueKey = configResolution.values.issueKey || void 0;
1526
+ if (projectKey && env.TESTOPS_FORGE_ENDPOINT?.trim()) {
1527
+ try {
1528
+ const [testCasesResult, bddArtifactsResult] = await Promise.all([
1529
+ context.invokeForgeContract("listTestCases", {
1530
+ context: { projectKey, issueKey }
1531
+ }),
1532
+ context.invokeForgeContract("listBddArtifacts", {
1533
+ context: { projectKey, issueKey }
1534
+ })
1535
+ ]);
1536
+ if (testCasesResult.ok && bddArtifactsResult.ok) {
1537
+ remoteSnapshot = {
1538
+ testCases: testCasesResult.data,
1539
+ bddArtifacts: bddArtifactsResult.data
1540
+ };
1541
+ }
1542
+ } catch {
1543
+ }
1544
+ }
1545
+ const featureDiscoveryForMapping = discoverFeatureFiles(scanResult.scannedFiles);
1546
+ const discoveredArtifacts = discoverArtifacts(scanResult.scannedFiles);
1547
+ const mappingPreview = buildMappingPreview({
1548
+ rootPath,
1549
+ featurePaths: featureDiscoveryForMapping.paths,
1550
+ contextIssueKey: issueKey,
1551
+ remoteSnapshot
1552
+ });
1553
+ let execution;
1554
+ let exitCode = ExitCode.Success;
1555
+ if (!dryRun) {
1556
+ if (!projectKey) {
1557
+ const diagnostic = diagMissingProjectContext();
1558
+ return {
1559
+ exitCode: ExitCode.ValidationError,
1560
+ stderr: [
1561
+ `ERROR: ${diagnostic.code}: ${diagnostic.message}`,
1562
+ `SUGGESTION: ${diagnostic.suggestion}`
1563
+ ]
1564
+ };
1565
+ }
1566
+ execution = await executeUploadFlow({
1567
+ rootPath,
1568
+ projectKey,
1569
+ issueKey,
1570
+ featurePaths: featureDiscoveryForMapping.paths,
1571
+ artifactList: discoveredArtifacts.artifacts,
1572
+ mappingPreview,
1573
+ force,
1574
+ context
1575
+ });
1576
+ if (!execution.executed && execution.blockedReason) {
1577
+ exitCode = ExitCode.ValidationError;
1578
+ } else if (execution.featureFailed + execution.runFailed > 0) {
1579
+ exitCode = ExitCode.RemoteError;
1580
+ } else if (execution.featureSuccess + execution.runSuccess === 0) {
1581
+ exitCode = ExitCode.ValidationError;
1582
+ }
1583
+ }
1584
+ const summary = buildSummary({
1585
+ rootPath,
1586
+ dryRun,
1587
+ include,
1588
+ exclude,
1589
+ output,
1590
+ maxDepth: scanResult.maxDepth,
1591
+ scannedFiles: scanResult.scannedFiles,
1592
+ env,
1593
+ mappingPreview,
1594
+ execution
1595
+ });
1596
+ if (output === "json" || useJson) {
1597
+ return {
1598
+ exitCode,
1599
+ stdout: toJsonLine(summary)
1600
+ };
1601
+ }
1602
+ return {
1603
+ exitCode,
1604
+ stdout: toHumanLines(summary)
1605
+ };
1606
+ };
1607
+ }
1608
+
1609
+ // src/forgeClient.ts
1610
+ var ForgeClientError = class extends Error {
1611
+ code;
1612
+ status;
1613
+ retryable;
1614
+ constructor(input) {
1615
+ super(input.message);
1616
+ this.name = "ForgeClientError";
1617
+ this.code = input.code;
1618
+ this.status = input.status;
1619
+ this.retryable = Boolean(input.retryable);
1620
+ }
1621
+ };
1622
+ function isObject(value) {
1623
+ return Boolean(value) && typeof value === "object";
1624
+ }
1625
+ function isServiceError(value) {
1626
+ if (!isObject(value)) {
1627
+ return false;
1628
+ }
1629
+ const code = value.code;
1630
+ const message = value.message;
1631
+ return typeof code === "string" && typeof message === "string";
1632
+ }
1633
+ function normalizeServiceError(error) {
1634
+ return new ForgeClientError({
1635
+ code: error.code,
1636
+ message: error.message,
1637
+ retryable: false
1638
+ });
1639
+ }
1640
+ function normalizeUnknownResponseError(status, message) {
1641
+ return new ForgeClientError({
1642
+ code: "HTTP_ERROR",
1643
+ status,
1644
+ message,
1645
+ retryable: status === 429 || status >= 500
1646
+ });
1647
+ }
1648
+ function delay(ms) {
1649
+ return new Promise((resolve) => setTimeout(resolve, ms));
1650
+ }
1651
+ function normalizeEndpoint(endpoint) {
1652
+ return endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
1653
+ }
1654
+ function shouldRetry(error, attempt, maxRetries) {
1655
+ if (attempt >= maxRetries) {
1656
+ return false;
1657
+ }
1658
+ return error.retryable;
1659
+ }
1660
+ var ForgeApiClient = class {
1661
+ endpoint;
1662
+ authToken;
1663
+ timeoutMs;
1664
+ maxRetries;
1665
+ retryDelayMs;
1666
+ fetchImpl;
1667
+ constructor(options) {
1668
+ this.endpoint = normalizeEndpoint(options.endpoint);
1669
+ this.authToken = options.authToken ?? "";
1670
+ this.timeoutMs = options.timeoutMs ?? 5e3;
1671
+ this.maxRetries = options.maxRetries ?? 2;
1672
+ this.retryDelayMs = options.retryDelayMs ?? 250;
1673
+ this.fetchImpl = options.fetchImpl ?? fetch;
1674
+ }
1675
+ async invoke(contract, payload) {
1676
+ let attempt = 0;
1677
+ while (true) {
1678
+ try {
1679
+ return await this.invokeOnce(contract, payload);
1680
+ } catch (error) {
1681
+ const normalized = this.normalizeThrownError(error);
1682
+ if (!shouldRetry(normalized, attempt, this.maxRetries)) {
1683
+ throw normalized;
1684
+ }
1685
+ attempt += 1;
1686
+ const backoff = this.retryDelayMs * attempt;
1687
+ await delay(backoff);
1688
+ }
1689
+ }
1690
+ }
1691
+ async invokeOnce(contract, payload) {
1692
+ const abortController = new AbortController();
1693
+ const timeout = setTimeout(() => abortController.abort(), this.timeoutMs);
1694
+ try {
1695
+ const response = await this.fetchImpl(this.endpoint, {
1696
+ method: "POST",
1697
+ signal: abortController.signal,
1698
+ headers: {
1699
+ "content-type": "application/json",
1700
+ ...this.authToken ? { authorization: `Bearer ${this.authToken}` } : {}
1701
+ },
1702
+ body: JSON.stringify({
1703
+ contract,
1704
+ payload
1705
+ })
1706
+ });
1707
+ const text = await response.text();
1708
+ let parsed = {};
1709
+ if (text) {
1710
+ try {
1711
+ parsed = JSON.parse(text);
1712
+ } catch {
1713
+ throw new ForgeClientError({
1714
+ code: "INVALID_RESPONSE_ERROR",
1715
+ message: "Forge endpoint returned invalid JSON payload.",
1716
+ status: response.status,
1717
+ retryable: false
1718
+ });
1719
+ }
1720
+ }
1721
+ if (!response.ok) {
1722
+ if (isObject(parsed) && "error" in parsed && isServiceError(parsed.error)) {
1723
+ const errorShape = parsed.error;
1724
+ throw normalizeServiceError(errorShape);
1725
+ }
1726
+ throw normalizeUnknownResponseError(
1727
+ response.status,
1728
+ `Forge endpoint request failed with HTTP ${response.status}.`
1729
+ );
1730
+ }
1731
+ if (isObject(parsed) && "ok" in parsed && parsed.ok === false && "error" in parsed && isServiceError(parsed.error)) {
1732
+ throw normalizeServiceError(parsed.error);
1733
+ }
1734
+ return parsed;
1735
+ } finally {
1736
+ clearTimeout(timeout);
1737
+ }
1738
+ }
1739
+ normalizeThrownError(error) {
1740
+ if (error instanceof ForgeClientError) {
1741
+ return error;
1742
+ }
1743
+ if (error instanceof Error && error.name === "AbortError") {
1744
+ return new ForgeClientError({
1745
+ code: "TIMEOUT_ERROR",
1746
+ message: `Forge endpoint timed out after ${this.timeoutMs}ms.`,
1747
+ retryable: true
1748
+ });
1749
+ }
1750
+ return new ForgeClientError({
1751
+ code: "NETWORK_ERROR",
1752
+ message: error instanceof Error ? error.message : "Unknown network error while calling Forge endpoint.",
1753
+ retryable: true
1754
+ });
1755
+ }
1756
+ };
1757
+ function createForgeApiClient(options) {
1758
+ return new ForgeApiClient(options);
1759
+ }
1760
+
1761
+ // src/bdd.ts
1762
+ var CONFIG_FLAGS2 = /* @__PURE__ */ new Set([
1763
+ "--config",
1764
+ "--base-url",
1765
+ "--project-key",
1766
+ "--issue-key",
1767
+ "--auth-mode",
1768
+ "--jira-email",
1769
+ "--jira-api-token"
1770
+ ]);
1771
+ function parseArgs2(args) {
1772
+ const flags = {};
1773
+ const boolFlags = /* @__PURE__ */ new Set();
1774
+ const unknownFlags = [];
1775
+ const supportedValueFlags = /* @__PURE__ */ new Set(["--id", ...CONFIG_FLAGS2]);
1776
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
1777
+ for (let index = 0; index < args.length; index += 1) {
1778
+ const token = args[index];
1779
+ if (!token.startsWith("--")) {
1780
+ continue;
1781
+ }
1782
+ if (supportedBoolFlags.has(token)) {
1783
+ boolFlags.add(token);
1784
+ continue;
1785
+ }
1786
+ if (!supportedValueFlags.has(token)) {
1787
+ unknownFlags.push(token);
1788
+ continue;
1789
+ }
1790
+ const value = args[index + 1];
1791
+ if (!value || value.startsWith("--")) {
1792
+ unknownFlags.push(token);
1793
+ continue;
1794
+ }
1795
+ flags[token] = value;
1796
+ index += 1;
1797
+ }
1798
+ return { flags, boolFlags, unknownFlags };
1799
+ }
1800
+ function pickConfigArgs2(parsed) {
1801
+ const args = [];
1802
+ for (const [flag, value] of Object.entries(parsed.flags)) {
1803
+ if (CONFIG_FLAGS2.has(flag)) {
1804
+ args.push(flag, value);
1805
+ }
1806
+ }
1807
+ return args;
1808
+ }
1809
+ function normalizeError(error) {
1810
+ if (error instanceof ForgeClientError) {
1811
+ return `${error.code}: ${error.message}`;
1812
+ }
1813
+ if (error instanceof Error) {
1814
+ return error.message;
1815
+ }
1816
+ return "Unknown bdd command error.";
1817
+ }
1818
+ function toJsonScenario(scenario) {
1819
+ return {
1820
+ id: scenario.id,
1821
+ featureId: scenario.featureId,
1822
+ featureName: scenario.featureName,
1823
+ name: scenario.name,
1824
+ tags: [...scenario.tags],
1825
+ linkedIssueKeys: [...scenario.linkedIssueKeys],
1826
+ steps: scenario.steps.map((step) => ({
1827
+ keyword: step.keyword,
1828
+ text: step.text
1829
+ })),
1830
+ createdAt: scenario.createdAt,
1831
+ updatedAt: scenario.updatedAt
1832
+ };
1833
+ }
1834
+ function scenarioToLines(scenario) {
1835
+ const lines = [
1836
+ `BDD scenario: ${scenario.name}`,
1837
+ `Scenario id: ${scenario.id}`,
1838
+ `Feature: ${scenario.featureName}`,
1839
+ `Tags: ${scenario.tags.join(", ") || "none"}`,
1840
+ `Linked issues: ${scenario.linkedIssueKeys.join(", ") || "none"}`,
1841
+ `Created: ${scenario.createdAt}`,
1842
+ `Updated: ${scenario.updatedAt}`,
1843
+ "Steps:"
1844
+ ];
1845
+ for (const step of scenario.steps) {
1846
+ lines.push(`- ${step.keyword} ${step.text}`);
1847
+ }
1848
+ return lines;
1849
+ }
1850
+ function missingProjectResponse() {
1851
+ return {
1852
+ exitCode: ExitCode.ValidationError,
1853
+ stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
1854
+ };
1855
+ }
1856
+ function createBddHandler(deps = {}) {
1857
+ const cwd = deps.cwd ?? process.cwd();
1858
+ const env = deps.env ?? process.env;
1859
+ return async (request, context) => {
1860
+ const [subcommand, ...restArgs] = request.args;
1861
+ const parsed = parseArgs2(restArgs);
1862
+ const useJson = parsed.boolFlags.has("--json");
1863
+ const config = resolveCliConfig(pickConfigArgs2(parsed), env, cwd);
1864
+ const projectKey = config.values.projectKey;
1865
+ const issueKey = config.values.issueKey;
1866
+ if (parsed.unknownFlags.length > 0) {
1867
+ return {
1868
+ exitCode: ExitCode.UsageError,
1869
+ stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
1870
+ };
1871
+ }
1872
+ if (!projectKey) {
1873
+ return missingProjectResponse();
1874
+ }
1875
+ if (subcommand === "scenarios") {
1876
+ const nested = restArgs[0];
1877
+ if (nested !== "show") {
1878
+ return {
1879
+ exitCode: ExitCode.UsageError,
1880
+ stderr: ["ERROR: Unsupported bdd scenarios subcommand. Use: testops bdd scenarios show --id <SCENARIO_ID>"]
1881
+ };
1882
+ }
1883
+ const scenarioId = parsed.flags["--id"]?.trim() ?? "";
1884
+ if (!scenarioId) {
1885
+ return {
1886
+ exitCode: ExitCode.ValidationError,
1887
+ stderr: ["ERROR: Missing required --id for testops bdd scenarios show."]
1888
+ };
1889
+ }
1890
+ try {
1891
+ const result = await context.invokeForgeContract("getBddScenario", {
1892
+ context: {
1893
+ projectKey,
1894
+ issueKey: issueKey || void 0
1895
+ },
1896
+ scenarioId
1897
+ });
1898
+ if (!result.ok) {
1899
+ return {
1900
+ exitCode: ExitCode.RemoteError,
1901
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
1902
+ };
1903
+ }
1904
+ if (useJson) {
1905
+ return {
1906
+ exitCode: ExitCode.Success,
1907
+ stdout: toJsonLine({
1908
+ action: "bdd-scenarios-show",
1909
+ projectKey,
1910
+ issueKey: issueKey || null,
1911
+ item: toJsonScenario(result.data)
1912
+ })
1913
+ };
1914
+ }
1915
+ return {
1916
+ exitCode: ExitCode.Success,
1917
+ stdout: scenarioToLines(result.data)
1918
+ };
1919
+ } catch (error) {
1920
+ return {
1921
+ exitCode: ExitCode.TransportError,
1922
+ stderr: [`ERROR: ${normalizeError(error)}`]
1923
+ };
1924
+ }
1925
+ }
1926
+ return {
1927
+ exitCode: ExitCode.UsageError,
1928
+ stderr: [`ERROR: Unsupported bdd subcommand: ${subcommand}`]
1929
+ };
1930
+ };
1931
+ }
1932
+
1933
+ // src/cases.ts
1934
+ var CONFIG_FLAGS3 = /* @__PURE__ */ new Set([
1935
+ "--config",
1936
+ "--base-url",
1937
+ "--project-key",
1938
+ "--issue-key",
1939
+ "--auth-mode",
1940
+ "--jira-email",
1941
+ "--jira-api-token"
1942
+ ]);
1943
+ function parseArgs3(args) {
1944
+ const flags = {};
1945
+ const boolFlags = /* @__PURE__ */ new Set();
1946
+ const unknownFlags = [];
1947
+ const supportedValueFlags = /* @__PURE__ */ new Set(["--key", ...CONFIG_FLAGS3]);
1948
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
1949
+ for (let index = 0; index < args.length; index += 1) {
1950
+ const token = args[index];
1951
+ if (!token.startsWith("--")) {
1952
+ continue;
1953
+ }
1954
+ if (supportedBoolFlags.has(token)) {
1955
+ boolFlags.add(token);
1956
+ continue;
1957
+ }
1958
+ if (!supportedValueFlags.has(token)) {
1959
+ unknownFlags.push(token);
1960
+ continue;
1961
+ }
1962
+ const value = args[index + 1];
1963
+ if (!value || value.startsWith("--")) {
1964
+ unknownFlags.push(token);
1965
+ continue;
1966
+ }
1967
+ flags[token] = value;
1968
+ index += 1;
1969
+ }
1970
+ return { flags, boolFlags, unknownFlags };
1971
+ }
1972
+ function pickConfigArgs3(parsed) {
1973
+ const args = [];
1974
+ for (const [flag, value] of Object.entries(parsed.flags)) {
1975
+ if (CONFIG_FLAGS3.has(flag)) {
1976
+ args.push(flag, value);
1977
+ }
1978
+ }
1979
+ return args;
1980
+ }
1981
+ function normalizeError2(error) {
1982
+ if (error instanceof ForgeClientError) {
1983
+ return `${error.code}: ${error.message}`;
1984
+ }
1985
+ if (error instanceof Error) {
1986
+ return error.message;
1987
+ }
1988
+ return "Unknown cases command error.";
1989
+ }
1990
+ function toJsonCase(testCase) {
1991
+ return {
1992
+ id: testCase.id,
1993
+ key: testCase.key,
1994
+ title: testCase.title,
1995
+ status: testCase.status,
1996
+ description: testCase.description ?? null,
1997
+ linkedIssueKeys: [...testCase.linkedIssueKeys ?? []],
1998
+ createdAt: testCase.createdAt,
1999
+ updatedAt: testCase.updatedAt
2000
+ };
2001
+ }
2002
+ function toJsonScenario2(scenario) {
2003
+ return {
2004
+ id: scenario.id,
2005
+ featureId: scenario.featureId,
2006
+ featureName: scenario.featureName,
2007
+ name: scenario.name,
2008
+ tags: [...scenario.tags],
2009
+ linkedIssueKeys: [...scenario.linkedIssueKeys],
2010
+ steps: scenario.steps.map((step) => ({
2011
+ keyword: step.keyword,
2012
+ text: step.text
2013
+ })),
2014
+ createdAt: scenario.createdAt,
2015
+ updatedAt: scenario.updatedAt
2016
+ };
2017
+ }
2018
+ function toJsonRun(run) {
2019
+ return {
2020
+ id: run.id,
2021
+ featureId: run.featureId ?? null,
2022
+ scenarioId: run.scenarioId ?? null,
2023
+ testCaseIds: [...run.testCaseIds ?? []],
2024
+ linkedIssueKeys: [...run.linkedIssueKeys],
2025
+ createdAt: run.createdAt,
2026
+ payload: {
2027
+ featureName: run.payload.featureName,
2028
+ scenarioName: run.payload.scenarioName,
2029
+ executedAt: run.payload.executedAt,
2030
+ steps: run.payload.steps.map((step) => ({
2031
+ step: step.step,
2032
+ result: step.result,
2033
+ error: step.error ?? null
2034
+ }))
2035
+ },
2036
+ executionSummary: {
2037
+ passed: run.executionSummary.passed,
2038
+ failed: run.executionSummary.failed,
2039
+ skipped: run.executionSummary.skipped,
2040
+ blocked: run.executionSummary.blocked,
2041
+ total: run.executionSummary.total
2042
+ }
2043
+ };
2044
+ }
2045
+ function listToLines(items) {
2046
+ if (items.length === 0) {
2047
+ return ["Test cases: 0", "No test cases found for the selected context."];
2048
+ }
2049
+ return [
2050
+ `Test cases: ${items.length}`,
2051
+ ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
2052
+ ];
2053
+ }
2054
+ function showToLines(testCase) {
2055
+ return [
2056
+ `Test case: ${testCase.key}`,
2057
+ `Title: ${testCase.title}`,
2058
+ `Status: ${testCase.status}`,
2059
+ `Description: ${testCase.description ?? "none"}`,
2060
+ `Linked issues: ${(testCase.linkedIssueKeys ?? []).join(", ") || "none"}`,
2061
+ `Created: ${testCase.createdAt}`,
2062
+ `Updated: ${testCase.updatedAt}`
2063
+ ];
2064
+ }
2065
+ function scenariosToLines(testCase, scenarios) {
2066
+ if (scenarios.length === 0) {
2067
+ return [
2068
+ `Linked scenarios: 0`,
2069
+ `Test case: ${testCase.key} ${testCase.title}`,
2070
+ "No linked BDD scenarios were found for this test case."
2071
+ ];
2072
+ }
2073
+ return [
2074
+ `Linked scenarios: ${scenarios.length}`,
2075
+ `Test case: ${testCase.key} ${testCase.title}`,
2076
+ ...scenarios.map((scenario) => {
2077
+ const tagSuffix = scenario.tags.length > 0 ? ` tags=${scenario.tags.join(",")}` : "";
2078
+ return `- ${scenario.featureName} :: ${scenario.name} (${scenario.steps.length} steps${tagSuffix})`;
2079
+ })
2080
+ ];
2081
+ }
2082
+ function runsToLines(testCase, runs) {
2083
+ if (runs.length === 0) {
2084
+ return [
2085
+ `Linked runs: 0`,
2086
+ `Test case: ${testCase.key} ${testCase.title}`,
2087
+ "No execution runs were found for this test case."
2088
+ ];
2089
+ }
2090
+ return [
2091
+ `Linked runs: ${runs.length}`,
2092
+ `Test case: ${testCase.key} ${testCase.title}`,
2093
+ ...runs.map(
2094
+ (run) => `- ${run.payload.executedAt} ${run.payload.featureName} :: ${run.payload.scenarioName} [pass=${run.executionSummary.passed} fail=${run.executionSummary.failed} skip=${run.executionSummary.skipped} blocked=${run.executionSummary.blocked}]`
2095
+ )
2096
+ ];
2097
+ }
2098
+ function missingProjectResponse2() {
2099
+ return {
2100
+ exitCode: ExitCode.ValidationError,
2101
+ stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
2102
+ };
2103
+ }
2104
+ function missingKeyResponse(commandPath = "testops cases show") {
2105
+ return {
2106
+ exitCode: ExitCode.ValidationError,
2107
+ stderr: [`ERROR: Missing required --key for ${commandPath}.`]
2108
+ };
2109
+ }
2110
+ function createCasesHandler(deps = {}) {
2111
+ const cwd = deps.cwd ?? process.cwd();
2112
+ const env = deps.env ?? process.env;
2113
+ return async (request, context) => {
2114
+ const [subcommand, ...restArgs] = request.args;
2115
+ const parsed = parseArgs3(restArgs);
2116
+ const useJson = parsed.boolFlags.has("--json");
2117
+ const config = resolveCliConfig(pickConfigArgs3(parsed), env, cwd);
2118
+ const projectKey = config.values.projectKey;
2119
+ const issueKey = config.values.issueKey;
2120
+ if (parsed.unknownFlags.length > 0) {
2121
+ return {
2122
+ exitCode: ExitCode.UsageError,
2123
+ stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
2124
+ };
2125
+ }
2126
+ if (!projectKey) {
2127
+ return missingProjectResponse2();
2128
+ }
2129
+ if (subcommand === "list") {
2130
+ try {
2131
+ const result = await context.invokeForgeContract("listTestCases", {
2132
+ context: {
2133
+ projectKey,
2134
+ issueKey: issueKey || void 0
2135
+ }
2136
+ });
2137
+ if (!result.ok) {
2138
+ return {
2139
+ exitCode: ExitCode.RemoteError,
2140
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
2141
+ };
2142
+ }
2143
+ if (useJson) {
2144
+ return {
2145
+ exitCode: ExitCode.Success,
2146
+ stdout: toJsonLine({
2147
+ action: "cases-list",
2148
+ projectKey,
2149
+ issueKey: issueKey || null,
2150
+ count: result.data.length,
2151
+ items: result.data.map((item) => toJsonCase(item))
2152
+ })
2153
+ };
2154
+ }
2155
+ return {
2156
+ exitCode: ExitCode.Success,
2157
+ stdout: listToLines(result.data)
2158
+ };
2159
+ } catch (error) {
2160
+ return {
2161
+ exitCode: ExitCode.TransportError,
2162
+ stderr: [`ERROR: ${normalizeError2(error)}`]
2163
+ };
2164
+ }
2165
+ }
2166
+ if (subcommand === "show") {
2167
+ const testCaseKey = parsed.flags["--key"]?.trim().toUpperCase() ?? "";
2168
+ if (!testCaseKey) {
2169
+ return missingKeyResponse();
2170
+ }
2171
+ const payload = {
2172
+ context: {
2173
+ projectKey,
2174
+ issueKey: issueKey || void 0
2175
+ },
2176
+ testCaseKey
2177
+ };
2178
+ try {
2179
+ const result = await context.invokeForgeContract("getTestCase", payload);
2180
+ if (!result.ok) {
2181
+ return {
2182
+ exitCode: ExitCode.RemoteError,
2183
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
2184
+ };
2185
+ }
2186
+ if (useJson) {
2187
+ return {
2188
+ exitCode: ExitCode.Success,
2189
+ stdout: toJsonLine({
2190
+ action: "cases-show",
2191
+ projectKey,
2192
+ issueKey: issueKey || null,
2193
+ item: toJsonCase(result.data)
2194
+ })
2195
+ };
2196
+ }
2197
+ return {
2198
+ exitCode: ExitCode.Success,
2199
+ stdout: showToLines(result.data)
2200
+ };
2201
+ } catch (error) {
2202
+ return {
2203
+ exitCode: ExitCode.TransportError,
2204
+ stderr: [`ERROR: ${normalizeError2(error)}`]
2205
+ };
2206
+ }
2207
+ }
2208
+ if (subcommand === "bdd") {
2209
+ const nested = restArgs[0];
2210
+ if (nested !== "list") {
2211
+ return {
2212
+ exitCode: ExitCode.UsageError,
2213
+ stderr: ["ERROR: Unsupported cases bdd subcommand. Use: testops cases bdd list --key <TEST_CASE_KEY>"]
2214
+ };
2215
+ }
2216
+ const testCaseKey = parsed.flags["--key"]?.trim().toUpperCase() ?? "";
2217
+ if (!testCaseKey) {
2218
+ return missingKeyResponse("testops cases bdd list");
2219
+ }
2220
+ try {
2221
+ const testCaseResult = await context.invokeForgeContract("getTestCase", {
2222
+ context: {
2223
+ projectKey,
2224
+ issueKey: issueKey || void 0
2225
+ },
2226
+ testCaseKey
2227
+ });
2228
+ if (!testCaseResult.ok) {
2229
+ return {
2230
+ exitCode: ExitCode.RemoteError,
2231
+ stderr: [`ERROR: ${testCaseResult.error.code}: ${testCaseResult.error.message}`]
2232
+ };
2233
+ }
2234
+ const scenariosResult = await context.invokeForgeContract("listBddScenarios", {
2235
+ context: {
2236
+ projectKey,
2237
+ issueKey: issueKey || void 0
2238
+ },
2239
+ testCaseId: testCaseResult.data.id
2240
+ });
2241
+ if (!scenariosResult.ok) {
2242
+ return {
2243
+ exitCode: ExitCode.RemoteError,
2244
+ stderr: [`ERROR: ${scenariosResult.error.code}: ${scenariosResult.error.message}`]
2245
+ };
2246
+ }
2247
+ if (useJson) {
2248
+ return {
2249
+ exitCode: ExitCode.Success,
2250
+ stdout: toJsonLine({
2251
+ action: "cases-bdd-list",
2252
+ projectKey,
2253
+ issueKey: issueKey || null,
2254
+ testCase: toJsonCase(testCaseResult.data),
2255
+ count: scenariosResult.data.length,
2256
+ items: scenariosResult.data.map((scenario) => toJsonScenario2(scenario))
2257
+ })
2258
+ };
2259
+ }
2260
+ return {
2261
+ exitCode: ExitCode.Success,
2262
+ stdout: scenariosToLines(testCaseResult.data, scenariosResult.data)
2263
+ };
2264
+ } catch (error) {
2265
+ return {
2266
+ exitCode: ExitCode.TransportError,
2267
+ stderr: [`ERROR: ${normalizeError2(error)}`]
2268
+ };
2269
+ }
2270
+ }
2271
+ if (subcommand === "runs") {
2272
+ const nested = restArgs[0];
2273
+ if (nested !== "list") {
2274
+ return {
2275
+ exitCode: ExitCode.UsageError,
2276
+ stderr: ["ERROR: Unsupported cases runs subcommand. Use: testops cases runs list --key <TEST_CASE_KEY>"]
2277
+ };
2278
+ }
2279
+ const testCaseKey = parsed.flags["--key"]?.trim().toUpperCase() ?? "";
2280
+ if (!testCaseKey) {
2281
+ return missingKeyResponse("testops cases runs list");
2282
+ }
2283
+ try {
2284
+ const testCaseResult = await context.invokeForgeContract("getTestCase", {
2285
+ context: {
2286
+ projectKey,
2287
+ issueKey: issueKey || void 0
2288
+ },
2289
+ testCaseKey
2290
+ });
2291
+ if (!testCaseResult.ok) {
2292
+ return {
2293
+ exitCode: ExitCode.RemoteError,
2294
+ stderr: [`ERROR: ${testCaseResult.error.code}: ${testCaseResult.error.message}`]
2295
+ };
2296
+ }
2297
+ const runsResult = await context.invokeForgeContract("listRunsByTestCase", {
2298
+ context: {
2299
+ projectKey,
2300
+ issueKey: issueKey || void 0
2301
+ },
2302
+ testCaseId: testCaseResult.data.id
2303
+ });
2304
+ if (!runsResult.ok) {
2305
+ return {
2306
+ exitCode: ExitCode.RemoteError,
2307
+ stderr: [`ERROR: ${runsResult.error.code}: ${runsResult.error.message}`]
2308
+ };
2309
+ }
2310
+ if (useJson) {
2311
+ return {
2312
+ exitCode: ExitCode.Success,
2313
+ stdout: toJsonLine({
2314
+ action: "cases-runs-list",
2315
+ projectKey,
2316
+ issueKey: issueKey || null,
2317
+ testCase: toJsonCase(testCaseResult.data),
2318
+ count: runsResult.data.length,
2319
+ items: runsResult.data.map((run) => toJsonRun(run))
2320
+ })
2321
+ };
2322
+ }
2323
+ return {
2324
+ exitCode: ExitCode.Success,
2325
+ stdout: runsToLines(testCaseResult.data, runsResult.data)
2326
+ };
2327
+ } catch (error) {
2328
+ return {
2329
+ exitCode: ExitCode.TransportError,
2330
+ stderr: [`ERROR: ${normalizeError2(error)}`]
2331
+ };
2332
+ }
2333
+ }
2334
+ return {
2335
+ exitCode: ExitCode.UsageError,
2336
+ stderr: [`ERROR: Unsupported cases subcommand: ${subcommand}`]
2337
+ };
2338
+ };
2339
+ }
2340
+
2341
+ // src/doctor.ts
2342
+ var CONFIG_FLAGS4 = /* @__PURE__ */ new Set([
2343
+ "--config",
2344
+ "--base-url",
2345
+ "--project-key",
2346
+ "--issue-key",
2347
+ "--auth-mode",
2348
+ "--jira-email",
2349
+ "--jira-api-token"
2350
+ ]);
2351
+ function parseArgs4(args) {
2352
+ const flags = {};
2353
+ const boolFlags = /* @__PURE__ */ new Set();
2354
+ const unknownFlags = [];
2355
+ const valuedFlags = /* @__PURE__ */ new Set([...CONFIG_FLAGS4]);
2356
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--json", "--check-context"]);
2357
+ for (let i = 0; i < args.length; i += 1) {
2358
+ const token = args[i];
2359
+ if (!token.startsWith("--")) {
2360
+ continue;
2361
+ }
2362
+ if (supportedBoolFlags.has(token)) {
2363
+ boolFlags.add(token);
2364
+ continue;
2365
+ }
2366
+ if (!valuedFlags.has(token)) {
2367
+ unknownFlags.push(token);
2368
+ continue;
2369
+ }
2370
+ const value = args[i + 1];
2371
+ if (!value || value.startsWith("--")) {
2372
+ unknownFlags.push(token);
2373
+ continue;
2374
+ }
2375
+ flags[token] = value;
2376
+ i += 1;
2377
+ }
2378
+ return { flags, boolFlags, unknownFlags };
2379
+ }
2380
+ function pickConfigArgs4(parsed) {
2381
+ const configArgs = [];
2382
+ for (const [flag, value] of Object.entries(parsed.flags)) {
2383
+ if (CONFIG_FLAGS4.has(flag)) {
2384
+ configArgs.push(flag, value);
2385
+ }
2386
+ }
2387
+ return configArgs;
2388
+ }
2389
+ function computeOverallStatus(checks) {
2390
+ if (checks.some((check) => check.status === "fail")) {
2391
+ return "fail";
2392
+ }
2393
+ if (checks.some((check) => check.status === "warn")) {
2394
+ return "warn";
2395
+ }
2396
+ return "pass";
2397
+ }
2398
+ function normalizeConnectivityError(error) {
2399
+ if (error instanceof ForgeClientError) {
2400
+ return `${error.code}: ${error.message}`;
2401
+ }
2402
+ if (error instanceof Error) {
2403
+ return error.message;
2404
+ }
2405
+ return "unknown connectivity error";
2406
+ }
2407
+ function checkFromServiceResult(result) {
2408
+ if (result.ok) {
2409
+ return {
2410
+ name: "forge_connectivity",
2411
+ status: "pass",
2412
+ message: "Forge contract call succeeded."
2413
+ };
2414
+ }
2415
+ return {
2416
+ name: "forge_connectivity",
2417
+ status: "pass",
2418
+ message: `Forge endpoint reachable (service returned ${result.error.code}).`
2419
+ };
2420
+ }
2421
+ function toJsonPayload(summary) {
2422
+ return {
2423
+ status: summary.status,
2424
+ checks: summary.checks.map((check) => ({
2425
+ name: check.name,
2426
+ status: check.status,
2427
+ message: check.message
2428
+ }))
2429
+ };
2430
+ }
2431
+ function pickDoctorExitCode(summary) {
2432
+ if (summary.status !== "fail") {
2433
+ return ExitCode.Success;
2434
+ }
2435
+ if (summary.checks.some((check) => check.name === "arguments" && check.status === "fail")) {
2436
+ return ExitCode.UsageError;
2437
+ }
2438
+ if (summary.checks.some((check) => check.name === "forge_connectivity" && check.status === "fail")) {
2439
+ return ExitCode.TransportError;
2440
+ }
2441
+ return ExitCode.ValidationError;
2442
+ }
2443
+ function toHumanLines2(summary) {
2444
+ const lines = [`Doctor preflight: ${summary.status.toUpperCase()}`];
2445
+ for (const check of summary.checks) {
2446
+ lines.push(`- [${check.status.toUpperCase()}] ${check.name}: ${check.message}`);
2447
+ }
2448
+ return lines;
2449
+ }
2450
+ function createDoctorHandler(deps = {}) {
2451
+ const cwd = deps.cwd ?? process.cwd();
2452
+ const env = deps.env ?? process.env;
2453
+ return async (request, context) => {
2454
+ const [, ...subArgs] = request.args;
2455
+ const parsed = parseArgs4(subArgs);
2456
+ const useJson = parsed.boolFlags.has("--json");
2457
+ const enforceContextCheck = parsed.boolFlags.has("--check-context");
2458
+ const checks = [];
2459
+ if (parsed.unknownFlags.length > 0) {
2460
+ checks.push({
2461
+ name: "arguments",
2462
+ status: "fail",
2463
+ message: `Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`
2464
+ });
2465
+ const summary2 = { status: computeOverallStatus(checks), checks };
2466
+ return {
2467
+ exitCode: pickDoctorExitCode(summary2),
2468
+ stdout: useJson ? toJsonLine(toJsonPayload(summary2)) : toHumanLines2(summary2)
2469
+ };
2470
+ }
2471
+ const configArgs = pickConfigArgs4(parsed);
2472
+ const resolution = resolveCliConfig(configArgs, env, cwd);
2473
+ const configValidation = validateCliConfigResolution(resolution);
2474
+ checks.push({
2475
+ name: "config_completeness",
2476
+ status: configValidation.ok ? "pass" : "fail",
2477
+ message: configValidation.ok ? "Required config is present." : configValidation.errors.join(" ")
2478
+ });
2479
+ if (configValidation.warnings.length > 0) {
2480
+ checks.push({
2481
+ name: "config_warnings",
2482
+ status: "warn",
2483
+ message: configValidation.warnings.join(" ")
2484
+ });
2485
+ }
2486
+ const endpoint = env.TESTOPS_FORGE_ENDPOINT?.trim() ?? "";
2487
+ if (!endpoint) {
2488
+ checks.push({
2489
+ name: "forge_endpoint",
2490
+ status: "fail",
2491
+ message: "Missing TESTOPS_FORGE_ENDPOINT."
2492
+ });
2493
+ } else {
2494
+ checks.push({
2495
+ name: "forge_endpoint",
2496
+ status: "pass",
2497
+ message: "Forge endpoint is configured."
2498
+ });
2499
+ }
2500
+ const hasContext = Boolean(
2501
+ resolution.values.projectKey || resolution.values.issueKey
2502
+ );
2503
+ if (enforceContextCheck || hasContext) {
2504
+ if (!hasContext) {
2505
+ checks.push({
2506
+ name: "jira_context",
2507
+ status: "warn",
2508
+ message: "Context check requested but no JIRA_PROJECT_KEY/JIRA_ISSUE_KEY provided."
2509
+ });
2510
+ } else {
2511
+ checks.push({
2512
+ name: "jira_context",
2513
+ status: "pass",
2514
+ message: `Using context project=${resolution.values.projectKey || "<unset>"} issue=${resolution.values.issueKey || "<unset>"}.`
2515
+ });
2516
+ }
2517
+ } else {
2518
+ checks.push({
2519
+ name: "jira_context",
2520
+ status: "skip",
2521
+ message: "Optional context check skipped. Use --check-context to enforce."
2522
+ });
2523
+ }
2524
+ if (endpoint) {
2525
+ try {
2526
+ const connectivity = await context.invokeForgeContract("listTestCases", {
2527
+ context: {
2528
+ projectKey: resolution.values.projectKey || void 0,
2529
+ issueKey: resolution.values.issueKey || void 0
2530
+ }
2531
+ });
2532
+ checks.push(checkFromServiceResult(connectivity));
2533
+ } catch (error) {
2534
+ checks.push({
2535
+ name: "forge_connectivity",
2536
+ status: "fail",
2537
+ message: normalizeConnectivityError(error)
2538
+ });
2539
+ }
2540
+ } else {
2541
+ checks.push({
2542
+ name: "forge_connectivity",
2543
+ status: "skip",
2544
+ message: "Skipped because Forge endpoint is not configured."
2545
+ });
2546
+ }
2547
+ const summary = {
2548
+ status: computeOverallStatus(checks),
2549
+ checks
2550
+ };
2551
+ return {
2552
+ exitCode: pickDoctorExitCode(summary),
2553
+ stdout: useJson ? toJsonLine(toJsonPayload(summary)) : toHumanLines2(summary)
2554
+ };
2555
+ };
2556
+ }
2557
+
2558
+ // src/ingestFeature.ts
2559
+ import { readFileSync as readFileSync4 } from "node:fs";
2560
+ import path6 from "node:path";
2561
+ var CONFIG_FLAGS5 = /* @__PURE__ */ new Set([
2562
+ "--config",
2563
+ "--base-url",
2564
+ "--project-key",
2565
+ "--issue-key",
2566
+ "--auth-mode",
2567
+ "--jira-email",
2568
+ "--jira-api-token"
2569
+ ]);
2570
+ function parseArgs5(args) {
2571
+ const flags = {};
2572
+ const boolFlags = /* @__PURE__ */ new Set();
2573
+ const unknownFlags = [];
2574
+ const valuedFlags = /* @__PURE__ */ new Set(["--file", "--name", ...CONFIG_FLAGS5]);
2575
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--stdin", "--json"]);
2576
+ for (let i = 0; i < args.length; i += 1) {
2577
+ const token = args[i];
2578
+ if (!token.startsWith("--")) {
2579
+ continue;
2580
+ }
2581
+ if (supportedBoolFlags.has(token)) {
2582
+ boolFlags.add(token);
2583
+ continue;
2584
+ }
2585
+ if (!valuedFlags.has(token)) {
2586
+ unknownFlags.push(token);
2587
+ continue;
2588
+ }
2589
+ const value = args[i + 1];
2590
+ if (!value || value.startsWith("--")) {
2591
+ unknownFlags.push(token);
2592
+ continue;
2593
+ }
2594
+ flags[token] = value;
2595
+ i += 1;
2596
+ }
2597
+ return { flags, boolFlags, unknownFlags };
2598
+ }
2599
+ function deriveNameFromGherkin(gherkin) {
2600
+ const lines = gherkin.split(/\r?\n/);
2601
+ for (const line of lines) {
2602
+ const trimmed = line.trim();
2603
+ if (trimmed.toLowerCase().startsWith("feature:")) {
2604
+ return trimmed.slice("feature:".length).trim();
2605
+ }
2606
+ }
2607
+ return "";
2608
+ }
2609
+ function pickConfigArgs5(parsed) {
2610
+ const configArgs = [];
2611
+ for (const [flag, value] of Object.entries(parsed.flags)) {
2612
+ if (CONFIG_FLAGS5.has(flag)) {
2613
+ configArgs.push(flag, value);
2614
+ }
2615
+ }
2616
+ return configArgs;
2617
+ }
2618
+ function summarizeSuccess(result) {
2619
+ if (!result.ok) {
2620
+ return [];
2621
+ }
2622
+ const payload = result.data;
2623
+ if (!payload || typeof payload !== "object") {
2624
+ return [];
2625
+ }
2626
+ const asRecord = payload;
2627
+ const lines = [];
2628
+ if (typeof asRecord.id === "string" && asRecord.id) {
2629
+ lines.push(`Artifact id: ${asRecord.id}`);
2630
+ }
2631
+ if (typeof asRecord.projectKey === "string" && asRecord.projectKey) {
2632
+ lines.push(`Project: ${asRecord.projectKey}`);
2633
+ }
2634
+ return lines;
2635
+ }
2636
+ function normalizeError3(error) {
2637
+ if (error instanceof ForgeClientError) {
2638
+ return `${error.code}: ${error.message}`;
2639
+ }
2640
+ if (error instanceof Error) {
2641
+ return error.message;
2642
+ }
2643
+ return "Unknown ingest error.";
2644
+ }
2645
+ function createIngestFeatureHandler(deps = {}) {
2646
+ const readFile = deps.readFile ?? ((filePath) => readFileSync4(filePath, "utf8"));
2647
+ const readStdin = deps.readStdin ?? (() => readFileSync4(0, "utf8"));
2648
+ const cwd = deps.cwd ?? process.cwd();
2649
+ const env = deps.env ?? process.env;
2650
+ return async (request, context) => {
2651
+ const [, ...subArgs] = request.args;
2652
+ const parsed = parseArgs5(subArgs);
2653
+ const configArgs = pickConfigArgs5(parsed);
2654
+ const config = resolveCliConfig(configArgs, env, cwd);
2655
+ const projectKey = config.values.projectKey;
2656
+ const issueKey = config.values.issueKey;
2657
+ const sourceFile = parsed.flags["--file"] ? path6.resolve(cwd, parsed.flags["--file"]) : "";
2658
+ const useStdin = parsed.boolFlags.has("--stdin");
2659
+ const useJson = parsed.boolFlags.has("--json");
2660
+ const sourceCount = Number(Boolean(sourceFile)) + Number(useStdin);
2661
+ if (sourceCount !== 1) {
2662
+ return {
2663
+ exitCode: ExitCode.ValidationError,
2664
+ stderr: ["ERROR: Provide exactly one input source: --file <path> or --stdin."]
2665
+ };
2666
+ }
2667
+ let gherkin = "";
2668
+ try {
2669
+ gherkin = useStdin ? readStdin() : readFile(sourceFile);
2670
+ } catch (error) {
2671
+ return {
2672
+ exitCode: ExitCode.InternalError,
2673
+ stderr: [`ERROR: Failed to read feature source: ${normalizeError3(error)}`]
2674
+ };
2675
+ }
2676
+ const name = (parsed.flags["--name"] ?? deriveNameFromGherkin(gherkin)).trim();
2677
+ if (!name) {
2678
+ return {
2679
+ exitCode: ExitCode.ValidationError,
2680
+ stderr: ["ERROR: Missing feature name. Pass --name or include 'Feature: <name>' in gherkin."]
2681
+ };
2682
+ }
2683
+ if (!gherkin.trim()) {
2684
+ return {
2685
+ exitCode: ExitCode.ValidationError,
2686
+ stderr: ["ERROR: Gherkin content is empty."]
2687
+ };
2688
+ }
2689
+ if (!projectKey) {
2690
+ return {
2691
+ exitCode: ExitCode.ValidationError,
2692
+ stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
2693
+ };
2694
+ }
2695
+ if (parsed.unknownFlags.length > 0) {
2696
+ return {
2697
+ exitCode: ExitCode.UsageError,
2698
+ stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
2699
+ };
2700
+ }
2701
+ const payload = {
2702
+ context: {
2703
+ projectKey,
2704
+ issueKey: issueKey || void 0
2705
+ },
2706
+ input: {
2707
+ name,
2708
+ gherkin
2709
+ }
2710
+ };
2711
+ try {
2712
+ const result = await context.invokeForgeContract("ingestBddFeature", payload);
2713
+ if (!result.ok) {
2714
+ if (useJson) {
2715
+ return {
2716
+ exitCode: ExitCode.RemoteError,
2717
+ stdout: toJsonLine({
2718
+ action: "ingest-feature",
2719
+ status: "failed",
2720
+ featureName: name,
2721
+ source: useStdin ? "stdin" : sourceFile,
2722
+ errorCode: result.error.code,
2723
+ errorMessage: result.error.message
2724
+ })
2725
+ };
2726
+ }
2727
+ return {
2728
+ exitCode: ExitCode.RemoteError,
2729
+ stdout: [
2730
+ "Feature ingestion: FAILED",
2731
+ `Feature: ${name}`,
2732
+ `Source: ${useStdin ? "stdin" : sourceFile}`
2733
+ ],
2734
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
2735
+ };
2736
+ }
2737
+ if (useJson) {
2738
+ const details = summarizeSuccess(result);
2739
+ return {
2740
+ exitCode: ExitCode.Success,
2741
+ stdout: toJsonLine({
2742
+ action: "ingest-feature",
2743
+ status: "success",
2744
+ featureName: name,
2745
+ source: useStdin ? "stdin" : sourceFile,
2746
+ details
2747
+ })
2748
+ };
2749
+ }
2750
+ return {
2751
+ exitCode: ExitCode.Success,
2752
+ stdout: [
2753
+ "Feature ingestion: SUCCESS",
2754
+ `Feature: ${name}`,
2755
+ `Source: ${useStdin ? "stdin" : sourceFile}`,
2756
+ ...summarizeSuccess(result)
2757
+ ]
2758
+ };
2759
+ } catch (error) {
2760
+ if (useJson) {
2761
+ return {
2762
+ exitCode: ExitCode.TransportError,
2763
+ stdout: toJsonLine({
2764
+ action: "ingest-feature",
2765
+ status: "failed",
2766
+ featureName: name,
2767
+ source: useStdin ? "stdin" : sourceFile,
2768
+ errorMessage: normalizeError3(error)
2769
+ })
2770
+ };
2771
+ }
2772
+ return {
2773
+ exitCode: ExitCode.TransportError,
2774
+ stdout: [
2775
+ "Feature ingestion: FAILED",
2776
+ `Feature: ${name}`,
2777
+ `Source: ${useStdin ? "stdin" : sourceFile}`
2778
+ ],
2779
+ stderr: [`ERROR: ${normalizeError3(error)}`]
2780
+ };
2781
+ }
2782
+ };
2783
+ }
2784
+
2785
+ // src/runUpload.ts
2786
+ import { readFileSync as readFileSync5 } from "node:fs";
2787
+ import path7 from "node:path";
2788
+ var STEP_RESULTS = [
2789
+ StepResult.Passed,
2790
+ StepResult.Failed,
2791
+ StepResult.Skipped,
2792
+ StepResult.Blocked
2793
+ ];
2794
+ var CONFIG_FLAGS6 = /* @__PURE__ */ new Set([
2795
+ "--config",
2796
+ "--base-url",
2797
+ "--project-key",
2798
+ "--issue-key",
2799
+ "--auth-mode",
2800
+ "--jira-email",
2801
+ "--jira-api-token"
2802
+ ]);
2803
+ function parseArgs6(args) {
2804
+ const flags = {};
2805
+ const boolFlags = /* @__PURE__ */ new Set();
2806
+ const unknownFlags = [];
2807
+ const valuedFlags = /* @__PURE__ */ new Set([
2808
+ "--file",
2809
+ "--feature-name",
2810
+ "--scenario-name",
2811
+ "--executed-at",
2812
+ ...CONFIG_FLAGS6
2813
+ ]);
2814
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--stdin", "--json"]);
2815
+ for (let i = 0; i < args.length; i += 1) {
2816
+ const token = args[i];
2817
+ if (!token.startsWith("--")) {
2818
+ continue;
2819
+ }
2820
+ if (supportedBoolFlags.has(token)) {
2821
+ boolFlags.add(token);
2822
+ continue;
2823
+ }
2824
+ if (!valuedFlags.has(token)) {
2825
+ unknownFlags.push(token);
2826
+ continue;
2827
+ }
2828
+ const value = args[i + 1];
2829
+ if (!value || value.startsWith("--")) {
2830
+ unknownFlags.push(token);
2831
+ continue;
2832
+ }
2833
+ flags[token] = value;
2834
+ i += 1;
2835
+ }
2836
+ return { flags, boolFlags, unknownFlags };
2837
+ }
2838
+ function pickConfigArgs6(parsed) {
2839
+ const configArgs = [];
2840
+ for (const [flag, value] of Object.entries(parsed.flags)) {
2841
+ if (CONFIG_FLAGS6.has(flag)) {
2842
+ configArgs.push(flag, value);
2843
+ }
2844
+ }
2845
+ return configArgs;
2846
+ }
2847
+ function normalizeError4(error) {
2848
+ if (error instanceof ForgeClientError) {
2849
+ return `${error.code}: ${error.message}`;
2850
+ }
2851
+ if (error instanceof Error) {
2852
+ return error.message;
2853
+ }
2854
+ return "Unknown run upload error.";
2855
+ }
2856
+ function parseRunPayload(raw) {
2857
+ try {
2858
+ const parsed = JSON.parse(raw);
2859
+ if (!parsed || typeof parsed !== "object") {
2860
+ return null;
2861
+ }
2862
+ return parsed;
2863
+ } catch {
2864
+ return null;
2865
+ }
2866
+ }
2867
+ function isIsoDateTime(value) {
2868
+ if (!value.trim()) {
2869
+ return false;
2870
+ }
2871
+ return !Number.isNaN(Date.parse(value));
2872
+ }
2873
+ function validateRunInput(input) {
2874
+ const errors = [];
2875
+ if (!input.featureName?.trim()) {
2876
+ errors.push("featureName is required.");
2877
+ }
2878
+ if (!input.scenarioName?.trim()) {
2879
+ errors.push("scenarioName is required.");
2880
+ }
2881
+ if (!isIsoDateTime(input.executedAt)) {
2882
+ errors.push("executedAt must be a valid ISO date-time.");
2883
+ }
2884
+ if (!Array.isArray(input.steps) || input.steps.length === 0) {
2885
+ errors.push("steps must contain at least one item.");
2886
+ return errors;
2887
+ }
2888
+ input.steps.forEach((step, index) => {
2889
+ if (!step?.step?.trim()) {
2890
+ errors.push(`steps[${index}].step is required.`);
2891
+ }
2892
+ if (!STEP_RESULTS.includes(step.result)) {
2893
+ errors.push(`steps[${index}].result must be one of: ${STEP_RESULTS.join(", ")}.`);
2894
+ }
2895
+ if (step.result === StepResult.Failed && !step.error?.trim()) {
2896
+ errors.push(`steps[${index}].error is required when result is failed.`);
2897
+ }
2898
+ });
2899
+ return errors;
2900
+ }
2901
+ function mergeMetadataOverrides(input, parsed) {
2902
+ return {
2903
+ featureName: (parsed.flags["--feature-name"] ?? input.featureName ?? "").trim(),
2904
+ scenarioName: (parsed.flags["--scenario-name"] ?? input.scenarioName ?? "").trim(),
2905
+ executedAt: (parsed.flags["--executed-at"] ?? input.executedAt ?? "").trim(),
2906
+ steps: Array.isArray(input.steps) ? input.steps : []
2907
+ };
2908
+ }
2909
+ function summarizeRunResult(result) {
2910
+ if (!result.ok || !result.data || typeof result.data !== "object") {
2911
+ return [];
2912
+ }
2913
+ const payload = result.data;
2914
+ const lines = [];
2915
+ if (typeof payload.id === "string" && payload.id) {
2916
+ lines.push(`Run artifact id: ${payload.id}`);
2917
+ }
2918
+ if (typeof payload.projectKey === "string" && payload.projectKey) {
2919
+ lines.push(`Project: ${payload.projectKey}`);
2920
+ }
2921
+ return lines;
2922
+ }
2923
+ function createRunUploadHandler(deps = {}) {
2924
+ const readFile = deps.readFile ?? ((filePath) => readFileSync5(filePath, "utf8"));
2925
+ const readStdin = deps.readStdin ?? (() => readFileSync5(0, "utf8"));
2926
+ const cwd = deps.cwd ?? process.cwd();
2927
+ const env = deps.env ?? process.env;
2928
+ return async (request, context) => {
2929
+ const [, ...subArgs] = request.args;
2930
+ const parsed = parseArgs6(subArgs);
2931
+ const configArgs = pickConfigArgs6(parsed);
2932
+ const config = resolveCliConfig(configArgs, env, cwd);
2933
+ const projectKey = config.values.projectKey;
2934
+ const issueKey = config.values.issueKey;
2935
+ const sourceFile = parsed.flags["--file"] ? path7.resolve(cwd, parsed.flags["--file"]) : "";
2936
+ const useStdin = parsed.boolFlags.has("--stdin");
2937
+ const useJson = parsed.boolFlags.has("--json");
2938
+ const sourceCount = Number(Boolean(sourceFile)) + Number(useStdin);
2939
+ if (sourceCount !== 1) {
2940
+ return {
2941
+ exitCode: ExitCode.ValidationError,
2942
+ stderr: ["ERROR: Provide exactly one input source: --file <path> or --stdin."]
2943
+ };
2944
+ }
2945
+ if (!projectKey) {
2946
+ return {
2947
+ exitCode: ExitCode.ValidationError,
2948
+ stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
2949
+ };
2950
+ }
2951
+ if (parsed.unknownFlags.length > 0) {
2952
+ return {
2953
+ exitCode: ExitCode.UsageError,
2954
+ stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
2955
+ };
2956
+ }
2957
+ let raw = "";
2958
+ try {
2959
+ raw = useStdin ? readStdin() : readFile(sourceFile);
2960
+ } catch (error) {
2961
+ return {
2962
+ exitCode: ExitCode.InternalError,
2963
+ stderr: [`ERROR: Failed to read run payload source: ${normalizeError4(error)}`]
2964
+ };
2965
+ }
2966
+ const payloadFromSource = parseRunPayload(raw);
2967
+ if (!payloadFromSource) {
2968
+ return {
2969
+ exitCode: ExitCode.ValidationError,
2970
+ stderr: ["ERROR: Run payload must be valid JSON matching BddRunInput shape."]
2971
+ };
2972
+ }
2973
+ const runInput = mergeMetadataOverrides(payloadFromSource, parsed);
2974
+ const validationErrors = validateRunInput(runInput);
2975
+ if (validationErrors.length > 0) {
2976
+ return {
2977
+ exitCode: ExitCode.ValidationError,
2978
+ stderr: validationErrors.map((line) => `ERROR: ${line}`)
2979
+ };
2980
+ }
2981
+ const payload = {
2982
+ context: {
2983
+ projectKey,
2984
+ issueKey: issueKey || void 0
2985
+ },
2986
+ input: runInput
2987
+ };
2988
+ try {
2989
+ const result = await context.invokeForgeContract("ingestBddRun", payload);
2990
+ if (!result.ok) {
2991
+ if (useJson) {
2992
+ return {
2993
+ exitCode: ExitCode.RemoteError,
2994
+ stdout: toJsonLine({
2995
+ action: "run-upload",
2996
+ status: "failed",
2997
+ featureName: runInput.featureName,
2998
+ scenarioName: runInput.scenarioName,
2999
+ executedAt: runInput.executedAt,
3000
+ errorCode: result.error.code,
3001
+ errorMessage: result.error.message
3002
+ })
3003
+ };
3004
+ }
3005
+ return {
3006
+ exitCode: ExitCode.RemoteError,
3007
+ stdout: [
3008
+ "Run upload: FAILED",
3009
+ `Feature: ${runInput.featureName}`,
3010
+ `Scenario: ${runInput.scenarioName}`,
3011
+ `ExecutedAt: ${runInput.executedAt}`
3012
+ ],
3013
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
3014
+ };
3015
+ }
3016
+ if (useJson) {
3017
+ return {
3018
+ exitCode: ExitCode.Success,
3019
+ stdout: toJsonLine({
3020
+ action: "run-upload",
3021
+ status: "success",
3022
+ featureName: runInput.featureName,
3023
+ scenarioName: runInput.scenarioName,
3024
+ executedAt: runInput.executedAt,
3025
+ stepCount: runInput.steps.length
3026
+ })
3027
+ };
3028
+ }
3029
+ return {
3030
+ exitCode: ExitCode.Success,
3031
+ stdout: [
3032
+ "Run upload: SUCCESS",
3033
+ `Feature: ${runInput.featureName}`,
3034
+ `Scenario: ${runInput.scenarioName}`,
3035
+ `ExecutedAt: ${runInput.executedAt}`,
3036
+ `Step count: ${runInput.steps.length}`,
3037
+ ...summarizeRunResult(result)
3038
+ ]
3039
+ };
3040
+ } catch (error) {
3041
+ if (useJson) {
3042
+ return {
3043
+ exitCode: ExitCode.TransportError,
3044
+ stdout: toJsonLine({
3045
+ action: "run-upload",
3046
+ status: "failed",
3047
+ featureName: runInput.featureName,
3048
+ scenarioName: runInput.scenarioName,
3049
+ executedAt: runInput.executedAt,
3050
+ errorMessage: normalizeError4(error)
3051
+ })
3052
+ };
3053
+ }
3054
+ return {
3055
+ exitCode: ExitCode.TransportError,
3056
+ stdout: [
3057
+ "Run upload: FAILED",
3058
+ `Feature: ${runInput.featureName}`,
3059
+ `Scenario: ${runInput.scenarioName}`,
3060
+ `ExecutedAt: ${runInput.executedAt}`
3061
+ ],
3062
+ stderr: [`ERROR: ${normalizeError4(error)}`]
3063
+ };
3064
+ }
3065
+ };
3066
+ }
3067
+
3068
+ // src/runs.ts
3069
+ var CONFIG_FLAGS7 = /* @__PURE__ */ new Set([
3070
+ "--config",
3071
+ "--base-url",
3072
+ "--project-key",
3073
+ "--issue-key",
3074
+ "--auth-mode",
3075
+ "--jira-email",
3076
+ "--jira-api-token"
3077
+ ]);
3078
+ function parseArgs7(args) {
3079
+ const flags = {};
3080
+ const boolFlags = /* @__PURE__ */ new Set();
3081
+ const unknownFlags = [];
3082
+ const supportedValueFlags = /* @__PURE__ */ new Set(["--id", ...CONFIG_FLAGS7]);
3083
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
3084
+ for (let index = 0; index < args.length; index += 1) {
3085
+ const token = args[index];
3086
+ if (!token.startsWith("--")) {
3087
+ continue;
3088
+ }
3089
+ if (supportedBoolFlags.has(token)) {
3090
+ boolFlags.add(token);
3091
+ continue;
3092
+ }
3093
+ if (!supportedValueFlags.has(token)) {
3094
+ unknownFlags.push(token);
3095
+ continue;
3096
+ }
3097
+ const value = args[index + 1];
3098
+ if (!value || value.startsWith("--")) {
3099
+ unknownFlags.push(token);
3100
+ continue;
3101
+ }
3102
+ flags[token] = value;
3103
+ index += 1;
3104
+ }
3105
+ return { flags, boolFlags, unknownFlags };
3106
+ }
3107
+ function pickConfigArgs7(parsed) {
3108
+ const args = [];
3109
+ for (const [flag, value] of Object.entries(parsed.flags)) {
3110
+ if (CONFIG_FLAGS7.has(flag)) {
3111
+ args.push(flag, value);
3112
+ }
3113
+ }
3114
+ return args;
3115
+ }
3116
+ function normalizeError5(error) {
3117
+ if (error instanceof ForgeClientError) {
3118
+ return `${error.code}: ${error.message}`;
3119
+ }
3120
+ if (error instanceof Error) {
3121
+ return error.message;
3122
+ }
3123
+ return "Unknown runs command error.";
3124
+ }
3125
+ function toJsonRun2(run) {
3126
+ return {
3127
+ id: run.id,
3128
+ featureId: run.featureId ?? null,
3129
+ scenarioId: run.scenarioId ?? null,
3130
+ testCaseIds: [...run.testCaseIds ?? []],
3131
+ linkedIssueKeys: [...run.linkedIssueKeys],
3132
+ createdAt: run.createdAt,
3133
+ payload: {
3134
+ featureName: run.payload.featureName,
3135
+ scenarioName: run.payload.scenarioName,
3136
+ executedAt: run.payload.executedAt,
3137
+ steps: run.payload.steps.map((step) => ({
3138
+ step: step.step,
3139
+ result: step.result,
3140
+ error: step.error ?? null
3141
+ }))
3142
+ },
3143
+ executionSummary: {
3144
+ passed: run.executionSummary.passed,
3145
+ failed: run.executionSummary.failed,
3146
+ skipped: run.executionSummary.skipped,
3147
+ blocked: run.executionSummary.blocked,
3148
+ total: run.executionSummary.total
3149
+ }
3150
+ };
3151
+ }
3152
+ function runToLines(run) {
3153
+ const lines = [
3154
+ `Run: ${run.id}`,
3155
+ `Feature: ${run.payload.featureName}`,
3156
+ `Scenario: ${run.payload.scenarioName}`,
3157
+ `Executed at: ${run.payload.executedAt}`,
3158
+ `Summary: pass=${run.executionSummary.passed} fail=${run.executionSummary.failed} skip=${run.executionSummary.skipped} blocked=${run.executionSummary.blocked} total=${run.executionSummary.total}`,
3159
+ `Linked issues: ${run.linkedIssueKeys.join(", ") || "none"}`,
3160
+ `Linked test cases: ${(run.testCaseIds ?? []).join(", ") || "none"}`,
3161
+ "Steps:"
3162
+ ];
3163
+ for (const step of run.payload.steps) {
3164
+ const errorSuffix = step.error ? ` error=${step.error}` : "";
3165
+ lines.push(`- ${step.result}: ${step.step}${errorSuffix}`);
3166
+ }
3167
+ return lines;
3168
+ }
3169
+ function missingProjectResponse3() {
3170
+ return {
3171
+ exitCode: ExitCode.ValidationError,
3172
+ stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
3173
+ };
3174
+ }
3175
+ function createRunsHandler(deps = {}) {
3176
+ const cwd = deps.cwd ?? process.cwd();
3177
+ const env = deps.env ?? process.env;
3178
+ return async (request, context) => {
3179
+ const [subcommand, ...restArgs] = request.args;
3180
+ const parsed = parseArgs7(restArgs);
3181
+ const useJson = parsed.boolFlags.has("--json");
3182
+ const config = resolveCliConfig(pickConfigArgs7(parsed), env, cwd);
3183
+ const projectKey = config.values.projectKey;
3184
+ const issueKey = config.values.issueKey;
3185
+ if (parsed.unknownFlags.length > 0) {
3186
+ return {
3187
+ exitCode: ExitCode.UsageError,
3188
+ stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
3189
+ };
3190
+ }
3191
+ if (!projectKey) {
3192
+ return missingProjectResponse3();
3193
+ }
3194
+ if (subcommand === "show") {
3195
+ const runId = parsed.flags["--id"]?.trim() ?? "";
3196
+ if (!runId) {
3197
+ return {
3198
+ exitCode: ExitCode.ValidationError,
3199
+ stderr: ["ERROR: Missing required --id for testops runs show."]
3200
+ };
3201
+ }
3202
+ try {
3203
+ const result = await context.invokeForgeContract("getRun", {
3204
+ context: {
3205
+ projectKey,
3206
+ issueKey: issueKey || void 0
3207
+ },
3208
+ runId
3209
+ });
3210
+ if (!result.ok) {
3211
+ return {
3212
+ exitCode: ExitCode.RemoteError,
3213
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
3214
+ };
3215
+ }
3216
+ if (useJson) {
3217
+ return {
3218
+ exitCode: ExitCode.Success,
3219
+ stdout: toJsonLine({
3220
+ action: "runs-show",
3221
+ projectKey,
3222
+ issueKey: issueKey || null,
3223
+ item: toJsonRun2(result.data)
3224
+ })
3225
+ };
3226
+ }
3227
+ return {
3228
+ exitCode: ExitCode.Success,
3229
+ stdout: runToLines(result.data)
3230
+ };
3231
+ } catch (error) {
3232
+ return {
3233
+ exitCode: ExitCode.TransportError,
3234
+ stderr: [`ERROR: ${normalizeError5(error)}`]
3235
+ };
3236
+ }
3237
+ }
3238
+ return {
3239
+ exitCode: ExitCode.UsageError,
3240
+ stderr: [`ERROR: Unsupported runs subcommand: ${subcommand}`]
3241
+ };
3242
+ };
3243
+ }
3244
+
3245
+ // src/suites.ts
3246
+ var CONFIG_FLAGS8 = /* @__PURE__ */ new Set([
3247
+ "--config",
3248
+ "--base-url",
3249
+ "--project-key",
3250
+ "--issue-key",
3251
+ "--auth-mode",
3252
+ "--jira-email",
3253
+ "--jira-api-token"
3254
+ ]);
3255
+ function parseArgs8(args) {
3256
+ const flags = {};
3257
+ const boolFlags = /* @__PURE__ */ new Set();
3258
+ const unknownFlags = [];
3259
+ const supportedValueFlags = /* @__PURE__ */ new Set(["--key", ...CONFIG_FLAGS8]);
3260
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
3261
+ for (let index = 0; index < args.length; index += 1) {
3262
+ const token = args[index];
3263
+ if (!token.startsWith("--")) {
3264
+ continue;
3265
+ }
3266
+ if (supportedBoolFlags.has(token)) {
3267
+ boolFlags.add(token);
3268
+ continue;
3269
+ }
3270
+ if (!supportedValueFlags.has(token)) {
3271
+ unknownFlags.push(token);
3272
+ continue;
3273
+ }
3274
+ const value = args[index + 1];
3275
+ if (!value || value.startsWith("--")) {
3276
+ unknownFlags.push(token);
3277
+ continue;
3278
+ }
3279
+ flags[token] = value;
3280
+ index += 1;
3281
+ }
3282
+ return { flags, boolFlags, unknownFlags };
3283
+ }
3284
+ function pickConfigArgs8(parsed) {
3285
+ const args = [];
3286
+ for (const [flag, value] of Object.entries(parsed.flags)) {
3287
+ if (CONFIG_FLAGS8.has(flag)) {
3288
+ args.push(flag, value);
3289
+ }
3290
+ }
3291
+ return args;
3292
+ }
3293
+ function normalizeError6(error) {
3294
+ if (error instanceof ForgeClientError) {
3295
+ return `${error.code}: ${error.message}`;
3296
+ }
3297
+ if (error instanceof Error) {
3298
+ return error.message;
3299
+ }
3300
+ return "Unknown suites command error.";
3301
+ }
3302
+ function toJsonSuite(suite) {
3303
+ return {
3304
+ id: suite.id,
3305
+ key: suite.key,
3306
+ name: suite.name,
3307
+ description: suite.description ?? null,
3308
+ linkedIssueKeys: [...suite.linkedIssueKeys ?? []],
3309
+ createdAt: suite.createdAt,
3310
+ updatedAt: suite.updatedAt
3311
+ };
3312
+ }
3313
+ function toJsonCase2(testCase) {
3314
+ return {
3315
+ id: testCase.id,
3316
+ key: testCase.key,
3317
+ title: testCase.title,
3318
+ status: testCase.status,
3319
+ description: testCase.description ?? null,
3320
+ linkedIssueKeys: [...testCase.linkedIssueKeys ?? []],
3321
+ createdAt: testCase.createdAt,
3322
+ updatedAt: testCase.updatedAt
3323
+ };
3324
+ }
3325
+ function suitesToLines(items) {
3326
+ if (items.length === 0) {
3327
+ return ["Test suites: 0", "No test suites found for the selected context."];
3328
+ }
3329
+ return [
3330
+ `Test suites: ${items.length}`,
3331
+ ...items.map((item) => `- ${item.key} ${item.name}`)
3332
+ ];
3333
+ }
3334
+ function suiteToLines(suite) {
3335
+ return [
3336
+ `Test suite: ${suite.key}`,
3337
+ `Name: ${suite.name}`,
3338
+ `Description: ${suite.description ?? "none"}`,
3339
+ `Linked issues: ${(suite.linkedIssueKeys ?? []).join(", ") || "none"}`,
3340
+ `Created: ${suite.createdAt}`,
3341
+ `Updated: ${suite.updatedAt}`
3342
+ ];
3343
+ }
3344
+ function suiteCasesToLines(suite, items) {
3345
+ if (items.length === 0) {
3346
+ return [
3347
+ `Suite test cases: 0`,
3348
+ `Suite: ${suite.key} ${suite.name}`,
3349
+ "No test cases are currently linked to this suite."
3350
+ ];
3351
+ }
3352
+ return [
3353
+ `Suite test cases: ${items.length}`,
3354
+ `Suite: ${suite.key} ${suite.name}`,
3355
+ ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
3356
+ ];
3357
+ }
3358
+ function missingProjectResponse4() {
3359
+ return {
3360
+ exitCode: ExitCode.ValidationError,
3361
+ stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
3362
+ };
3363
+ }
3364
+ function missingKeyResponse2(commandPath) {
3365
+ return {
3366
+ exitCode: ExitCode.ValidationError,
3367
+ stderr: [`ERROR: Missing required --key for ${commandPath}.`]
3368
+ };
3369
+ }
3370
+ function createSuitesHandler(deps = {}) {
3371
+ const cwd = deps.cwd ?? process.cwd();
3372
+ const env = deps.env ?? process.env;
3373
+ return async (request, context) => {
3374
+ const [subcommand, ...restArgs] = request.args;
3375
+ const parsed = parseArgs8(restArgs);
3376
+ const useJson = parsed.boolFlags.has("--json");
3377
+ const config = resolveCliConfig(pickConfigArgs8(parsed), env, cwd);
3378
+ const projectKey = config.values.projectKey;
3379
+ const issueKey = config.values.issueKey;
3380
+ if (parsed.unknownFlags.length > 0) {
3381
+ return {
3382
+ exitCode: ExitCode.UsageError,
3383
+ stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
3384
+ };
3385
+ }
3386
+ if (!projectKey) {
3387
+ return missingProjectResponse4();
3388
+ }
3389
+ if (subcommand === "list") {
3390
+ try {
3391
+ const result = await context.invokeForgeContract("listTestSuites", {
3392
+ context: {
3393
+ projectKey,
3394
+ issueKey: issueKey || void 0
3395
+ }
3396
+ });
3397
+ if (!result.ok) {
3398
+ return {
3399
+ exitCode: ExitCode.RemoteError,
3400
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
3401
+ };
3402
+ }
3403
+ if (useJson) {
3404
+ return {
3405
+ exitCode: ExitCode.Success,
3406
+ stdout: toJsonLine({
3407
+ action: "suites-list",
3408
+ projectKey,
3409
+ issueKey: issueKey || null,
3410
+ count: result.data.length,
3411
+ items: result.data.map((item) => toJsonSuite(item))
3412
+ })
3413
+ };
3414
+ }
3415
+ return {
3416
+ exitCode: ExitCode.Success,
3417
+ stdout: suitesToLines(result.data)
3418
+ };
3419
+ } catch (error) {
3420
+ return {
3421
+ exitCode: ExitCode.TransportError,
3422
+ stderr: [`ERROR: ${normalizeError6(error)}`]
3423
+ };
3424
+ }
3425
+ }
3426
+ const suiteKey = parsed.flags["--key"]?.trim().toUpperCase() ?? "";
3427
+ if (subcommand === "show") {
3428
+ if (!suiteKey) {
3429
+ return missingKeyResponse2("testops suites show");
3430
+ }
3431
+ try {
3432
+ const result = await context.invokeForgeContract("getTestSuite", {
3433
+ context: {
3434
+ projectKey,
3435
+ issueKey: issueKey || void 0
3436
+ },
3437
+ suiteKey
3438
+ });
3439
+ if (!result.ok) {
3440
+ return {
3441
+ exitCode: ExitCode.RemoteError,
3442
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
3443
+ };
3444
+ }
3445
+ if (useJson) {
3446
+ return {
3447
+ exitCode: ExitCode.Success,
3448
+ stdout: toJsonLine({
3449
+ action: "suites-show",
3450
+ projectKey,
3451
+ issueKey: issueKey || null,
3452
+ item: toJsonSuite(result.data)
3453
+ })
3454
+ };
3455
+ }
3456
+ return {
3457
+ exitCode: ExitCode.Success,
3458
+ stdout: suiteToLines(result.data)
3459
+ };
3460
+ } catch (error) {
3461
+ return {
3462
+ exitCode: ExitCode.TransportError,
3463
+ stderr: [`ERROR: ${normalizeError6(error)}`]
3464
+ };
3465
+ }
3466
+ }
3467
+ if (subcommand === "cases") {
3468
+ const nested = restArgs[0];
3469
+ if (nested !== "list") {
3470
+ return {
3471
+ exitCode: ExitCode.UsageError,
3472
+ stderr: ["ERROR: Unsupported suites cases subcommand. Use: testops suites cases list --key <SUITE_KEY>"]
3473
+ };
3474
+ }
3475
+ if (!suiteKey) {
3476
+ return missingKeyResponse2("testops suites cases list");
3477
+ }
3478
+ try {
3479
+ const [suiteResult, casesResult] = await Promise.all([
3480
+ context.invokeForgeContract("getTestSuite", {
3481
+ context: {
3482
+ projectKey,
3483
+ issueKey: issueKey || void 0
3484
+ },
3485
+ suiteKey
3486
+ }),
3487
+ context.invokeForgeContract("listSuiteTestCases", {
3488
+ context: {
3489
+ projectKey,
3490
+ issueKey: issueKey || void 0
3491
+ },
3492
+ suiteKey
3493
+ })
3494
+ ]);
3495
+ if (!suiteResult.ok) {
3496
+ return {
3497
+ exitCode: ExitCode.RemoteError,
3498
+ stderr: [`ERROR: ${suiteResult.error.code}: ${suiteResult.error.message}`]
3499
+ };
3500
+ }
3501
+ if (!casesResult.ok) {
3502
+ return {
3503
+ exitCode: ExitCode.RemoteError,
3504
+ stderr: [`ERROR: ${casesResult.error.code}: ${casesResult.error.message}`]
3505
+ };
3506
+ }
3507
+ if (useJson) {
3508
+ return {
3509
+ exitCode: ExitCode.Success,
3510
+ stdout: toJsonLine({
3511
+ action: "suites-cases-list",
3512
+ projectKey,
3513
+ issueKey: issueKey || null,
3514
+ suite: toJsonSuite(suiteResult.data),
3515
+ count: casesResult.data.length,
3516
+ items: casesResult.data.map((item) => toJsonCase2(item))
3517
+ })
3518
+ };
3519
+ }
3520
+ return {
3521
+ exitCode: ExitCode.Success,
3522
+ stdout: suiteCasesToLines(suiteResult.data, casesResult.data)
3523
+ };
3524
+ } catch (error) {
3525
+ return {
3526
+ exitCode: ExitCode.TransportError,
3527
+ stderr: [`ERROR: ${normalizeError6(error)}`]
3528
+ };
3529
+ }
3530
+ }
3531
+ return {
3532
+ exitCode: ExitCode.UsageError,
3533
+ stderr: [`ERROR: Unsupported suites subcommand: ${subcommand}`]
3534
+ };
3535
+ };
3536
+ }
3537
+
3538
+ // src/sync.ts
3539
+ var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
3540
+ "--config",
3541
+ "--base-url",
3542
+ "--project-key",
3543
+ "--issue-key",
3544
+ "--auth-mode",
3545
+ "--jira-email",
3546
+ "--jira-api-token"
3547
+ ]);
3548
+ var RECONCILE_CONFIRM_TOKEN = "RECONCILE";
3549
+ function parseArgs9(args) {
3550
+ const flags = {};
3551
+ const boolFlags = /* @__PURE__ */ new Set();
3552
+ const unknownFlags = [];
3553
+ const supportedValueFlags = /* @__PURE__ */ new Set(["--confirm", ...CONFIG_FLAGS9]);
3554
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
3555
+ for (let i = 0; i < args.length; i += 1) {
3556
+ const token = args[i];
3557
+ if (!token.startsWith("--")) {
3558
+ continue;
3559
+ }
3560
+ if (supportedBoolFlags.has(token)) {
3561
+ boolFlags.add(token);
3562
+ continue;
3563
+ }
3564
+ if (!supportedValueFlags.has(token)) {
3565
+ unknownFlags.push(token);
3566
+ continue;
3567
+ }
3568
+ const value = args[i + 1];
3569
+ if (!value || value.startsWith("--")) {
3570
+ unknownFlags.push(token);
3571
+ continue;
3572
+ }
3573
+ flags[token] = value;
3574
+ i += 1;
3575
+ }
3576
+ return { flags, boolFlags, unknownFlags };
3577
+ }
3578
+ function pickConfigArgs9(parsed) {
3579
+ const args = [];
3580
+ for (const [flag, value] of Object.entries(parsed.flags)) {
3581
+ if (CONFIG_FLAGS9.has(flag)) {
3582
+ args.push(flag, value);
3583
+ }
3584
+ }
3585
+ return args;
3586
+ }
3587
+ function normalizeError7(error) {
3588
+ if (error instanceof ForgeClientError) {
3589
+ return `${error.code}: ${error.message}`;
3590
+ }
3591
+ if (error instanceof Error) {
3592
+ return error.message;
3593
+ }
3594
+ return "Unknown sync command error.";
3595
+ }
3596
+ function statusToLines(status) {
3597
+ const counts = `Issues: ${status.issueCount}, test cases: ${status.testCaseCount}, features: ${status.bddFeatureCount}, runs: ${status.bddRunCount}`;
3598
+ const errorSummary = status.errorSummary ? `Error summary: conflict ${status.errorSummary.conflict}, mapping ${status.errorSummary.mapping}, validation ${status.errorSummary.validation}, internal ${status.errorSummary.internal}` : "Error summary: unavailable";
3599
+ return [
3600
+ "Sync status: SUCCESS",
3601
+ `Project: ${status.projectKey}`,
3602
+ `Last success: ${status.lastSuccessAt ?? "none"}`,
3603
+ `Updated at: ${status.updatedAt}`,
3604
+ counts,
3605
+ errorSummary,
3606
+ `Recent errors: ${status.recentErrors.length}`
3607
+ ];
3608
+ }
3609
+ function reconcileToLines(payload) {
3610
+ const optionalDetails = [];
3611
+ if (typeof payload.conflictCount === "number") {
3612
+ optionalDetails.push(`Conflicts: ${payload.conflictCount}`);
3613
+ }
3614
+ if (typeof payload.issueMappingsWritten === "number") {
3615
+ optionalDetails.push(`Issue mappings written: ${payload.issueMappingsWritten}`);
3616
+ }
3617
+ if (typeof payload.keyIndexEntriesWritten === "number") {
3618
+ optionalDetails.push(`Key-index entries written: ${payload.keyIndexEntriesWritten}`);
3619
+ }
3620
+ if (typeof payload.staleIssueMappingsRemoved === "number") {
3621
+ optionalDetails.push(`Stale issue mappings removed: ${payload.staleIssueMappingsRemoved}`);
3622
+ }
3623
+ if (typeof payload.staleKeyIndexEntriesRemoved === "number") {
3624
+ optionalDetails.push(`Stale key-index entries removed: ${payload.staleKeyIndexEntriesRemoved}`);
3625
+ }
3626
+ return [
3627
+ "Sync reconcile: SUCCESS",
3628
+ `Reconciled at: ${payload.reconciledAt}`,
3629
+ `Issue count: ${payload.issueCount}`,
3630
+ ...optionalDetails
3631
+ ];
3632
+ }
3633
+ function createSyncHandler(deps = {}) {
3634
+ const cwd = deps.cwd ?? process.cwd();
3635
+ const env = deps.env ?? process.env;
3636
+ return async (request, context) => {
3637
+ const [subcommand, ...restArgs] = request.args;
3638
+ const parsed = parseArgs9(restArgs);
3639
+ const useJson = parsed.boolFlags.has("--json");
3640
+ const config = resolveCliConfig(pickConfigArgs9(parsed), env, cwd);
3641
+ const projectKey = config.values.projectKey;
3642
+ const issueKey = config.values.issueKey;
3643
+ if (parsed.unknownFlags.length > 0) {
3644
+ return {
3645
+ exitCode: ExitCode.UsageError,
3646
+ stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
3647
+ };
3648
+ }
3649
+ if (!projectKey) {
3650
+ return {
3651
+ exitCode: ExitCode.ValidationError,
3652
+ stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
3653
+ };
3654
+ }
3655
+ if (subcommand === "status") {
3656
+ try {
3657
+ const result = await context.invokeForgeContract("getSyncStatus", {
3658
+ context: {
3659
+ projectKey,
3660
+ issueKey: issueKey || void 0
3661
+ }
3662
+ });
3663
+ if (!result.ok) {
3664
+ return {
3665
+ exitCode: ExitCode.RemoteError,
3666
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
3667
+ };
3668
+ }
3669
+ if (useJson) {
3670
+ return {
3671
+ exitCode: ExitCode.Success,
3672
+ stdout: toJsonLine({
3673
+ action: "sync-status",
3674
+ projectKey: result.data.projectKey,
3675
+ issueCount: result.data.issueCount,
3676
+ testCaseCount: result.data.testCaseCount,
3677
+ bddFeatureCount: result.data.bddFeatureCount,
3678
+ bddRunCount: result.data.bddRunCount,
3679
+ recentErrorCount: result.data.recentErrors.length
3680
+ })
3681
+ };
3682
+ }
3683
+ return {
3684
+ exitCode: ExitCode.Success,
3685
+ stdout: statusToLines(result.data)
3686
+ };
3687
+ } catch (error) {
3688
+ return {
3689
+ exitCode: ExitCode.TransportError,
3690
+ stderr: [`ERROR: ${normalizeError7(error)}`]
3691
+ };
3692
+ }
3693
+ }
3694
+ if (subcommand === "reconcile") {
3695
+ const confirmation = parsed.flags["--confirm"] ?? "";
3696
+ if (confirmation !== RECONCILE_CONFIRM_TOKEN) {
3697
+ return {
3698
+ exitCode: ExitCode.ValidationError,
3699
+ stderr: [
3700
+ `ERROR: Reconcile requires explicit confirmation.`,
3701
+ `Use: testops sync reconcile --confirm ${RECONCILE_CONFIRM_TOKEN}`
3702
+ ]
3703
+ };
3704
+ }
3705
+ try {
3706
+ const result = await context.invokeForgeContract("reconcileSync", {
3707
+ context: {
3708
+ projectKey,
3709
+ issueKey: issueKey || void 0
3710
+ }
3711
+ });
3712
+ if (!result.ok) {
3713
+ return {
3714
+ exitCode: ExitCode.RemoteError,
3715
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
3716
+ };
3717
+ }
3718
+ if (useJson) {
3719
+ return {
3720
+ exitCode: ExitCode.Success,
3721
+ stdout: toJsonLine({
3722
+ action: "sync-reconcile",
3723
+ reconciledAt: result.data.reconciledAt,
3724
+ issueCount: result.data.issueCount,
3725
+ conflictCount: result.data.conflictCount ?? 0
3726
+ })
3727
+ };
3728
+ }
3729
+ return {
3730
+ exitCode: ExitCode.Success,
3731
+ stdout: reconcileToLines(result.data)
3732
+ };
3733
+ } catch (error) {
3734
+ return {
3735
+ exitCode: ExitCode.TransportError,
3736
+ stderr: [`ERROR: ${normalizeError7(error)}`]
3737
+ };
3738
+ }
3739
+ }
3740
+ return {
3741
+ exitCode: ExitCode.UsageError,
3742
+ stderr: [`ERROR: Unsupported sync subcommand: ${subcommand}`]
3743
+ };
3744
+ };
3745
+ }
3746
+
3747
+ // src/registry.ts
3748
+ var COMMAND_REGISTRY = [
3749
+ {
3750
+ name: "auto",
3751
+ description: "Smart auto-ingestion command contract (MVP staged)",
3752
+ subcommands: [],
3753
+ directInvocation: true,
3754
+ handler: createAutoHandler()
3755
+ },
3756
+ {
3757
+ name: "bdd",
3758
+ description: "BDD scenario inspection commands",
3759
+ subcommands: ["scenarios"],
3760
+ handler: createBddHandler()
3761
+ },
3762
+ {
3763
+ name: "cases",
3764
+ description: "Hierarchy-aware test case browse commands",
3765
+ subcommands: ["list", "show", "bdd", "runs"],
3766
+ handler: createCasesHandler()
3767
+ },
3768
+ {
3769
+ name: "config",
3770
+ description: "Configuration and auth bootstrap commands",
3771
+ subcommands: ["show", "validate"],
3772
+ handler: createConfigHandler()
3773
+ },
3774
+ {
3775
+ name: "ingest",
3776
+ description: "BDD/test artifact ingestion commands",
3777
+ subcommands: ["feature"],
3778
+ handler: createIngestFeatureHandler()
3779
+ },
3780
+ {
3781
+ name: "run",
3782
+ description: "Execution result upload commands",
3783
+ subcommands: ["upload"],
3784
+ handler: createRunUploadHandler()
3785
+ },
3786
+ {
3787
+ name: "runs",
3788
+ description: "Execution result inspection commands",
3789
+ subcommands: ["show"],
3790
+ handler: createRunsHandler()
3791
+ },
3792
+ {
3793
+ name: "suites",
3794
+ description: "Hierarchy-aware suite browse commands",
3795
+ subcommands: ["list", "show", "cases"],
3796
+ handler: createSuitesHandler()
3797
+ },
3798
+ {
3799
+ name: "doctor",
3800
+ description: "Preflight and health checks",
3801
+ subcommands: ["check"],
3802
+ handler: createDoctorHandler()
3803
+ },
3804
+ {
3805
+ name: "sync",
3806
+ description: "Optional sync commands (architecture-gated)",
3807
+ subcommands: ["status", "reconcile"],
3808
+ gated: true,
3809
+ handler: createSyncHandler()
3810
+ }
3811
+ ];
3812
+ function createThinClientContext() {
3813
+ const endpoint = process.env.TESTOPS_FORGE_ENDPOINT?.trim() ?? "";
3814
+ const authToken = process.env.TESTOPS_FORGE_AUTH_TOKEN?.trim() ?? "";
3815
+ if (!endpoint) {
3816
+ return {
3817
+ invokeForgeContract: async (_contractName, _payload) => {
3818
+ throw new Error(
3819
+ "Missing TESTOPS_FORGE_ENDPOINT for Forge contract transport. Thin-client command handlers must call Forge contracts."
3820
+ );
3821
+ }
3822
+ };
3823
+ }
3824
+ const timeoutMs = Number(process.env.TESTOPS_FORGE_TIMEOUT_MS ?? "30000");
3825
+ const maxRetries = Number(process.env.TESTOPS_FORGE_MAX_RETRIES ?? "2");
3826
+ const retryDelayMs = Number(process.env.TESTOPS_FORGE_RETRY_DELAY_MS ?? "250");
3827
+ const client = createForgeApiClient({
3828
+ endpoint,
3829
+ authToken,
3830
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 3e4,
3831
+ maxRetries: Number.isFinite(maxRetries) && maxRetries >= 0 ? maxRetries : 2,
3832
+ retryDelayMs: Number.isFinite(retryDelayMs) && retryDelayMs > 0 ? retryDelayMs : 250
3833
+ });
3834
+ return {
3835
+ invokeForgeContract: (contractName, payload) => {
3836
+ return client.invoke(contractName, payload);
3837
+ }
3838
+ };
3839
+ }
3840
+
3841
+ // src/main.ts
3842
+ var HEADER = "Automatify TestOps CLI (Forge-first thin client)";
3843
+ var COMMAND_PREFIX = "automatify testops";
3844
+ var SYNC_GATE_HINT = "Enable optional sync commands with TESTOPS_ENABLE_SYNC=1.";
3845
+ function formatSubcommands(subcommands) {
3846
+ return subcommands.length === 0 ? "" : ` [${subcommands.join("|")}]`;
3847
+ }
3848
+ function findCommand(commandName, registry) {
3849
+ return registry.find((command) => command.name === commandName);
3850
+ }
3851
+ function listVisibleCommands(registry, experimentalSyncEnabled) {
3852
+ return registry.filter((command) => !command.gated || experimentalSyncEnabled);
3853
+ }
3854
+ function emitResponse(io, response) {
3855
+ for (const line of response.stdout ?? []) {
3856
+ io.log(line);
3857
+ }
3858
+ for (const line of response.stderr ?? []) {
3859
+ io.error(line);
3860
+ }
3861
+ }
3862
+ function buildGroupHelp(command) {
3863
+ return [
3864
+ `${command.name} commands`,
3865
+ "",
3866
+ `Usage: ${COMMAND_PREFIX} ${command.name}${formatSubcommands(command.subcommands)} [options]`,
3867
+ "",
3868
+ `Description: ${command.description}`,
3869
+ "",
3870
+ "Boundary:",
3871
+ " This command group is a thin wrapper over Forge contracts."
3872
+ ].join("\n");
3873
+ }
3874
+ function buildHelpText(registry = COMMAND_REGISTRY, experimentalSyncEnabled = false) {
3875
+ const visibleCommands = listVisibleCommands(registry, experimentalSyncEnabled);
3876
+ const lines = [
3877
+ HEADER,
3878
+ "",
3879
+ "Usage:",
3880
+ ` ${COMMAND_PREFIX} --help`,
3881
+ ` ${COMMAND_PREFIX} <command> [subcommand] [options]`,
3882
+ "",
3883
+ "Available command groups:"
3884
+ ];
3885
+ for (const command of visibleCommands) {
3886
+ lines.push(
3887
+ ` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`
3888
+ );
3889
+ }
3890
+ lines.push(
3891
+ "",
3892
+ "Boundary:",
3893
+ " Thin client over Forge contracts only.",
3894
+ " Command handlers must delegate to Forge resolver/webtrigger contracts."
3895
+ );
3896
+ if (!experimentalSyncEnabled) {
3897
+ lines.push("", `Optional gated group: sync (${SYNC_GATE_HINT})`);
3898
+ }
3899
+ return lines.join("\n");
3900
+ }
3901
+ function runCli(args, io = console, options = {}) {
3902
+ const experimentalSyncEnabled = options.experimentalSyncEnabled ?? process.env.TESTOPS_ENABLE_SYNC === "1";
3903
+ const registry = options.registry ?? COMMAND_REGISTRY;
3904
+ const context = options.context ?? createThinClientContext();
3905
+ const [groupArg, subcommandArg, ...restArgs] = args;
3906
+ if (!groupArg || groupArg === "--help" || groupArg === "-h" || groupArg === "help") {
3907
+ io.log(buildHelpText(registry, experimentalSyncEnabled));
3908
+ return Promise.resolve(ExitCode.Success);
3909
+ }
3910
+ const command = findCommand(groupArg, registry);
3911
+ if (!command) {
3912
+ io.error(`Unknown command group: ${groupArg}`);
3913
+ io.error(`Run \`${COMMAND_PREFIX} --help\` to see available command groups.`);
3914
+ return Promise.resolve(ExitCode.UsageError);
3915
+ }
3916
+ if (command.gated && !experimentalSyncEnabled) {
3917
+ io.error(`Command group '${command.name}' is disabled by default.`);
3918
+ io.error(SYNC_GATE_HINT);
3919
+ return Promise.resolve(ExitCode.UsageError);
3920
+ }
3921
+ if (command.directInvocation) {
3922
+ if (subcommandArg === "--help" || subcommandArg === "-h" || subcommandArg === "help") {
3923
+ io.log(buildAutoContractHelp());
3924
+ return Promise.resolve(ExitCode.Success);
3925
+ }
3926
+ const directArgs = subcommandArg ? [subcommandArg, ...restArgs] : [...restArgs];
3927
+ return Promise.resolve(
3928
+ command.handler(
3929
+ {
3930
+ group: command.name,
3931
+ args: directArgs,
3932
+ rawArgs: args
3933
+ },
3934
+ context
3935
+ )
3936
+ ).then((response) => {
3937
+ emitResponse(io, response);
3938
+ return response.exitCode;
3939
+ }).catch((error) => {
3940
+ const message = error instanceof Error ? error.message : "Unknown CLI execution error.";
3941
+ io.error(`Command execution failed: ${message}`);
3942
+ return ExitCode.InternalError;
3943
+ });
3944
+ }
3945
+ if (!subcommandArg || subcommandArg === "--help" || subcommandArg === "-h" || subcommandArg === "help") {
3946
+ io.log(buildGroupHelp(command));
3947
+ return Promise.resolve(ExitCode.Success);
3948
+ }
3949
+ if (!command.subcommands.includes(subcommandArg)) {
3950
+ io.error(`Unknown subcommand '${subcommandArg}' for '${command.name}'.`);
3951
+ io.error(`Run '${COMMAND_PREFIX} ${command.name} --help' to see supported subcommands.`);
3952
+ return Promise.resolve(ExitCode.UsageError);
3953
+ }
3954
+ return Promise.resolve(
3955
+ command.handler(
3956
+ {
3957
+ group: command.name,
3958
+ args: [subcommandArg, ...restArgs],
3959
+ rawArgs: args
3960
+ },
3961
+ context
3962
+ )
3963
+ ).then((response) => {
3964
+ emitResponse(io, response);
3965
+ return response.exitCode;
3966
+ }).catch((error) => {
3967
+ const message = error instanceof Error ? error.message : "Unknown CLI execution error.";
3968
+ io.error(`Command execution failed: ${message}`);
3969
+ return ExitCode.InternalError;
3970
+ });
3971
+ }
3972
+
3973
+ // src/index.ts
3974
+ var HEADER2 = "Automatify CLI";
3975
+ function showTopLevelHelp() {
3976
+ const lines = [
3977
+ HEADER2,
3978
+ "",
3979
+ "Usage:",
3980
+ " automatify --help",
3981
+ " automatify <product> [command] [options]",
3982
+ "",
3983
+ "Available products:",
3984
+ " testops TestOps \u2014 test management, BDD, execution, reporting",
3985
+ "",
3986
+ "Boundary:",
3987
+ " Thin client over Forge contracts only.",
3988
+ " No separate backend, no duplicate domain logic."
3989
+ ];
3990
+ for (const line of lines) {
3991
+ console.log(line);
3992
+ }
3993
+ }
3994
+ async function main() {
3995
+ const args = process.argv.slice(2);
3996
+ const [productArg, ...restArgs] = args;
3997
+ if (!productArg || productArg === "--help" || productArg === "-h" || productArg === "help") {
3998
+ showTopLevelHelp();
3999
+ process.exitCode = 0;
4000
+ return;
4001
+ }
4002
+ if (productArg !== "testops") {
4003
+ console.error(`Unknown product: ${productArg}`);
4004
+ console.error("Run `automatify --help` to see available products.");
4005
+ process.exitCode = 1;
4006
+ return;
4007
+ }
4008
+ const exitCode = await runCli(restArgs);
4009
+ process.exitCode = exitCode;
4010
+ }
4011
+ main();