@automatify-au/cli 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,6 +76,16 @@ Minimum:
76
76
  - `TESTOPS_FORGE_ENDPOINT` for commands that call Forge contracts (`doctor`, `ingest`, `run`, `sync`)
77
77
  - `TESTOPS_FORGE_AUTH_TOKEN` from the one-time token shown in Forge `Operations -> CLI access`
78
78
 
79
+ Local config helper:
80
+ ```bash
81
+ automatify testops config set baseUrl https://automatify-com-au.atlassian.net
82
+ automatify testops config set projectKey DEV
83
+ automatify testops config set forgeEndpoint "<webtrigger-url>"
84
+ printf "%s" "$TESTOPS_FORGE_AUTH_TOKEN" | automatify testops config set forgeAuthToken --stdin
85
+ ```
86
+
87
+ `forgeAuthToken` local storage currently uses macOS Keychain. On Windows, Linux, and CI, keep the token in `TESTOPS_FORGE_AUTH_TOKEN` instead. `.testops-cli.json` should store non-secret values only.
88
+
79
89
  Optional:
80
90
  - `TESTOPS_FORGE_TIMEOUT_MS`
81
91
  - `TESTOPS_FORGE_MAX_RETRIES`
@@ -91,10 +101,13 @@ All TestOps commands are invoked as `automatify testops <command>`:
91
101
  - `bdd`
92
102
  - `automatify testops bdd scenarios show --id <SCENARIO_ID>`
93
103
  - `automatify testops bdd scenarios show --id <SCENARIO_ID> --format feature`
104
+ - `automatify testops bdd scenarios show --id <SCENARIO_ID|SC-4> --feature`
94
105
  - `automatify testops bdd scenarios export --id <SCENARIO_ID|SC-4> --output-dir ./scenario-export`
95
106
  - `automatify testops bdd scenarios export --all --output-dir ./scenario-export`
96
107
  - `automatify testops bdd scenarios import --file ./scenario-export/SC-4.feature`
108
+ - `automatify testops bdd scenarios import --file ./scenario-export/SC-4.feature --dry-run`
97
109
  - `automatify testops bdd scenarios import --input-dir ./scenario-export`
110
+ - `automatify testops bdd scenarios import --input-dir ./scenario-export --dry-run`
98
111
  - `automatify testops bdd features export --output-dir ./features-export`
99
112
  - `automatify testops bdd features export --output-dir ./features-export --zip`
100
113
  - `automatify testops bdd features export --output-dir ./features-export --feature-id <FEATURE_ID>`
@@ -169,6 +182,19 @@ export JIRA_BASE_URL="https://automatify-com-au.atlassian.net"
169
182
  export JIRA_PROJECT_KEY="DEV"
170
183
  ```
171
184
 
185
+ Or store the non-secret values in `.testops-cli.json` and, on macOS, the token in Keychain:
186
+ ```bash
187
+ automatify testops config set baseUrl https://automatify-com-au.atlassian.net
188
+ automatify testops config set projectKey DEV
189
+ automatify testops config set forgeEndpoint "<webtrigger-url>"
190
+ printf "%s" "$TESTOPS_FORGE_AUTH_TOKEN" | automatify testops config set forgeAuthToken --stdin
191
+ ```
192
+
193
+ Windows, Linux, and CI should keep the token in environment variables:
194
+ ```bash
195
+ export TESTOPS_FORGE_AUTH_TOKEN="<one-time-token-from-forge-ui>"
196
+ ```
197
+
172
198
  Transport notes:
173
199
  - Bearer auth is required for operator/CI use. Forge stores only the token hash plus metadata.
174
200
  - Tokens are project-scoped, shown once at creation time, and can be revoked from the Forge UI.
@@ -307,6 +333,7 @@ automatify testops bdd scenarios import --project-key DEV --file ./artifacts/sce
307
333
 
308
334
  # Export every visible SC-* file and import the whole folder back later
309
335
  automatify testops bdd scenarios export --project-key DEV --all --output-dir ./artifacts/scenario-bulk
336
+ automatify testops bdd scenarios import --project-key DEV --input-dir ./artifacts/scenario-bulk --dry-run
310
337
  automatify testops bdd scenarios import --project-key DEV --input-dir ./artifacts/scenario-bulk
311
338
 
312
339
  # Re-import one exported file later
@@ -314,7 +341,7 @@ automatify testops ingest feature --project-key DEV --file ./artifacts/features-
314
341
  ```
315
342
 
316
343
  Feature export notes:
317
- - `bdd scenarios show --format feature` renders one scenario in feature-style text.
344
+ - `bdd scenarios show --format feature` and `bdd scenarios show --feature` render one scenario in feature-style text.
318
345
  - `bdd scenarios export` writes a single-scenario `SC-*.feature` file with a reserved `@testops-scenario-SC-*` tag.
319
346
  - Jira issue links are exported as normal Gherkin tags like `@DEV-2`; on import they are written back to `linkedIssueKeys`.
320
347
  - current model has `linked issues`, not a separate persisted `primary issue`, so all exported issue-key tags are treated as equal links.
@@ -1030,8 +1030,10 @@ var Priority;
1030
1030
 
1031
1031
  // src/config.ts
1032
1032
  var import_node_fs = require("node:fs");
1033
+ var import_node_child_process = require("node:child_process");
1033
1034
  var import_node_path = __toESM(require("node:path"), 1);
1034
1035
  var DEFAULT_CONFIG_FILENAME = ".testops-cli.json";
1036
+ var KEYCHAIN_SERVICE = "automatify-testops-cli";
1035
1037
  function parseFlags(args) {
1036
1038
  const flags = {};
1037
1039
  const unknownFlags = [];
@@ -1077,6 +1079,11 @@ function readConfigFile(configPath) {
1077
1079
  return {};
1078
1080
  }
1079
1081
  }
1082
+ function writeConfigFile(configPath, file) {
1083
+ (0, import_node_fs.writeFileSync)(configPath, `${JSON.stringify(file, null, 2)}
1084
+ `, "utf8");
1085
+ (0, import_node_fs.chmodSync)(configPath, 384);
1086
+ }
1080
1087
  function normalizeBaseUrl(value) {
1081
1088
  if (value.endsWith("/")) {
1082
1089
  return value.slice(0, -1);
@@ -1086,6 +1093,30 @@ function normalizeBaseUrl(value) {
1086
1093
  function readAuthMode(input) {
1087
1094
  return input === "api-token" ? "api-token" : "none";
1088
1095
  }
1096
+ function defaultKeychainAccount(configPath) {
1097
+ return import_node_path.default.resolve(configPath);
1098
+ }
1099
+ function readKeychainSecret(account) {
1100
+ if (!account || process.platform !== "darwin") {
1101
+ return "";
1102
+ }
1103
+ try {
1104
+ return (0, import_node_child_process.execFileSync)("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"], {
1105
+ encoding: "utf8",
1106
+ stdio: ["ignore", "pipe", "ignore"]
1107
+ }).trim();
1108
+ } catch {
1109
+ return "";
1110
+ }
1111
+ }
1112
+ function setKeychainSecret(account, value) {
1113
+ if (process.platform !== "darwin") {
1114
+ throw new Error("Keychain-backed secrets are only supported on macOS.");
1115
+ }
1116
+ (0, import_node_child_process.execFileSync)("security", ["add-generic-password", "-U", "-s", KEYCHAIN_SERVICE, "-a", account, "-w", value], {
1117
+ stdio: ["ignore", "ignore", "pipe"]
1118
+ });
1119
+ }
1089
1120
  function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "") {
1090
1121
  if (flagValue) {
1091
1122
  return { value: flagValue, source: "flag" };
@@ -1145,6 +1176,22 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
1145
1176
  toStringValue(env.JIRA_API_TOKEN),
1146
1177
  toStringValue(file.jiraApiToken ?? file.JIRA_API_TOKEN)
1147
1178
  );
1179
+ const forgeEndpointResolved = valueFromPrecedence(
1180
+ "forgeEndpoint",
1181
+ "",
1182
+ toStringValue(env.TESTOPS_FORGE_ENDPOINT),
1183
+ toStringValue(file.forgeEndpoint ?? file.TESTOPS_FORGE_ENDPOINT)
1184
+ );
1185
+ const forgeAuthTokenKeychainAccountResolved = valueFromPrecedence(
1186
+ "forgeAuthTokenKeychainAccount",
1187
+ "",
1188
+ "",
1189
+ toStringValue(file.forgeAuthTokenKeychainAccount)
1190
+ );
1191
+ const forgeAuthTokenFromEnv = toStringValue(env.TESTOPS_FORGE_AUTH_TOKEN);
1192
+ const forgeAuthTokenFromFile = toStringValue(file.forgeAuthToken ?? file.TESTOPS_FORGE_AUTH_TOKEN);
1193
+ const forgeAuthTokenFromKeychain = forgeAuthTokenFromEnv || forgeAuthTokenFromFile || !forgeAuthTokenKeychainAccountResolved.value ? "" : readKeychainSecret(forgeAuthTokenKeychainAccountResolved.value);
1194
+ const forgeAuthTokenResolved = forgeAuthTokenFromEnv ? { value: forgeAuthTokenFromEnv, source: "env" } : forgeAuthTokenFromFile ? { value: forgeAuthTokenFromFile, source: "file" } : forgeAuthTokenFromKeychain ? { value: forgeAuthTokenFromKeychain, source: "keychain" } : { value: "", source: "default" };
1148
1195
  const config = {
1149
1196
  baseUrl: normalizeBaseUrl(baseUrlResolved.value),
1150
1197
  projectKey: projectKeyResolved.value,
@@ -1152,6 +1199,9 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
1152
1199
  authMode: readAuthMode(authModeResolved.value),
1153
1200
  jiraEmail: jiraEmailResolved.value,
1154
1201
  jiraApiToken: jiraApiTokenResolved.value,
1202
+ forgeEndpoint: normalizeBaseUrl(forgeEndpointResolved.value),
1203
+ forgeAuthToken: forgeAuthTokenResolved.value,
1204
+ forgeAuthTokenKeychainAccount: forgeAuthTokenKeychainAccountResolved.value,
1155
1205
  configPath
1156
1206
  };
1157
1207
  const sources = {
@@ -1161,6 +1211,9 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
1161
1211
  authMode: authModeResolved.source,
1162
1212
  jiraEmail: jiraEmailResolved.source,
1163
1213
  jiraApiToken: jiraApiTokenResolved.source,
1214
+ forgeEndpoint: forgeEndpointResolved.source,
1215
+ forgeAuthToken: forgeAuthTokenResolved.source,
1216
+ forgeAuthTokenKeychainAccount: forgeAuthTokenKeychainAccountResolved.source,
1164
1217
  configPath: configPathSource
1165
1218
  };
1166
1219
  const warnings = [];
@@ -1218,9 +1271,78 @@ function toDisplayLines(resolution) {
1218
1271
  ` authMode: ${values.authMode} (${sources.authMode})`,
1219
1272
  ` jiraEmail: ${values.jiraEmail || "<unset>"} (${sources.jiraEmail})`,
1220
1273
  ` jiraApiToken: ${values.jiraApiToken ? maskSecret(values.jiraApiToken) : "<unset>"} (${sources.jiraApiToken})`,
1274
+ ` forgeEndpoint: ${values.forgeEndpoint || "<unset>"} (${sources.forgeEndpoint})`,
1275
+ ` forgeAuthToken: ${values.forgeAuthToken ? maskSecret(values.forgeAuthToken) : "<unset>"} (${sources.forgeAuthToken})`,
1276
+ ` forgeAuthTokenKeychainAccount: ${values.forgeAuthTokenKeychainAccount || "<unset>"} (${sources.forgeAuthTokenKeychainAccount})`,
1221
1277
  ` configPath: ${values.configPath} (${sources.configPath})`
1222
1278
  ];
1223
1279
  }
1280
+ function normalizeConfigSetKey(input) {
1281
+ switch (input) {
1282
+ case "baseUrl":
1283
+ case "JIRA_BASE_URL":
1284
+ return "baseUrl";
1285
+ case "projectKey":
1286
+ case "JIRA_PROJECT_KEY":
1287
+ return "projectKey";
1288
+ case "issueKey":
1289
+ case "JIRA_ISSUE_KEY":
1290
+ return "issueKey";
1291
+ case "authMode":
1292
+ case "TESTOPS_AUTH_MODE":
1293
+ return "authMode";
1294
+ case "jiraEmail":
1295
+ case "JIRA_EMAIL":
1296
+ return "jiraEmail";
1297
+ case "jiraApiToken":
1298
+ case "JIRA_API_TOKEN":
1299
+ return "jiraApiToken";
1300
+ case "forgeEndpoint":
1301
+ case "TESTOPS_FORGE_ENDPOINT":
1302
+ return "forgeEndpoint";
1303
+ case "forgeAuthToken":
1304
+ case "TESTOPS_FORGE_AUTH_TOKEN":
1305
+ return "forgeAuthToken";
1306
+ default:
1307
+ return "";
1308
+ }
1309
+ }
1310
+ function applyConfigSet(configPath, rawKey, value) {
1311
+ const key = normalizeConfigSetKey(rawKey);
1312
+ if (!key) {
1313
+ return {
1314
+ exitCode: ExitCode.UsageError,
1315
+ stderr: [
1316
+ "Unsupported config key. Use one of: baseUrl, projectKey, issueKey, authMode, jiraEmail, jiraApiToken, forgeEndpoint, forgeAuthToken."
1317
+ ]
1318
+ };
1319
+ }
1320
+ const file = readConfigFile(configPath);
1321
+ if (key === "forgeAuthToken") {
1322
+ const account = toStringValue(file.forgeAuthTokenKeychainAccount) || defaultKeychainAccount(configPath);
1323
+ setKeychainSecret(account, value);
1324
+ const nextFile2 = {
1325
+ ...file,
1326
+ forgeAuthToken: void 0,
1327
+ TESTOPS_FORGE_AUTH_TOKEN: void 0,
1328
+ forgeAuthTokenKeychainAccount: account
1329
+ };
1330
+ writeConfigFile(configPath, nextFile2);
1331
+ return {
1332
+ exitCode: ExitCode.Success,
1333
+ stdout: [`Config updated: forgeAuthToken stored in macOS Keychain (${account}).`]
1334
+ };
1335
+ }
1336
+ const nextFile = {
1337
+ ...file,
1338
+ [key]: key === "baseUrl" || key === "forgeEndpoint" ? normalizeBaseUrl(value) : value
1339
+ };
1340
+ writeConfigFile(configPath, nextFile);
1341
+ return {
1342
+ exitCode: ExitCode.Success,
1343
+ stdout: [`Config updated: ${rawKey}.`]
1344
+ };
1345
+ }
1224
1346
  function createConfigHandler(env = process.env, cwd = process.cwd()) {
1225
1347
  return (request) => {
1226
1348
  const [subcommand, ...subArgs] = request.args;
@@ -1245,6 +1367,18 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
1245
1367
  stderr: validation.errors.map((line) => `ERROR: ${line}`)
1246
1368
  };
1247
1369
  }
1370
+ if (subcommand === "set") {
1371
+ const [key, ...valueParts] = subArgs;
1372
+ const readValueFromStdin = key === "forgeAuthToken" && valueParts.length === 1 && valueParts[0] === "--stdin";
1373
+ const value = readValueFromStdin ? (0, import_node_fs.readFileSync)(0, "utf8").trim() : valueParts.join(" ").trim();
1374
+ if (!key || !value) {
1375
+ return {
1376
+ exitCode: ExitCode.UsageError,
1377
+ stderr: ["Usage: automatify testops config set <key> <value>"]
1378
+ };
1379
+ }
1380
+ return applyConfigSet(resolution.values.configPath, key, value);
1381
+ }
1248
1382
  return {
1249
1383
  exitCode: ExitCode.UsageError,
1250
1384
  stderr: [`Unsupported config subcommand: ${subcommand}`]
@@ -2761,7 +2895,7 @@ function parseArgs2(args) {
2761
2895
  "--file",
2762
2896
  ...CONFIG_FLAGS2
2763
2897
  ]);
2764
- const supportedBoolFlags = /* @__PURE__ */ new Set(["--json", "--zip", "--all"]);
2898
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--json", "--zip", "--all", "--dry-run", "--feature"]);
2765
2899
  for (let index = 0; index < args.length; index += 1) {
2766
2900
  const token = args[index];
2767
2901
  if (!token.startsWith("--")) {
@@ -2923,6 +3057,39 @@ function scenarioToLines(scenario) {
2923
3057
  }
2924
3058
  return lines;
2925
3059
  }
3060
+ async function resolveScenarioId(context, projectKey, issueKey, scenarioSelector) {
3061
+ const scenariosResult = await context.invokeForgeContract("listBddScenarios", {
3062
+ context: {
3063
+ projectKey,
3064
+ issueKey: issueKey || void 0
3065
+ }
3066
+ });
3067
+ if (!scenariosResult.ok) {
3068
+ return {
3069
+ ok: false,
3070
+ response: {
3071
+ exitCode: ExitCode.RemoteError,
3072
+ stderr: [`ERROR: ${scenariosResult.error.code}: ${scenariosResult.error.message}`]
3073
+ }
3074
+ };
3075
+ }
3076
+ const matchedScenario = scenariosResult.data.find(
3077
+ (item) => item.id === scenarioSelector || (item.key ?? "").toUpperCase() === scenarioSelector.toUpperCase()
3078
+ );
3079
+ if (!matchedScenario) {
3080
+ return {
3081
+ ok: false,
3082
+ response: {
3083
+ exitCode: ExitCode.RemoteError,
3084
+ stderr: [`ERROR: NOT_FOUND: BDD scenario ${scenarioSelector} was not found in this project.`]
3085
+ }
3086
+ };
3087
+ }
3088
+ return {
3089
+ ok: true,
3090
+ scenarioId: matchedScenario.id
3091
+ };
3092
+ }
2926
3093
  function featureToLines(feature) {
2927
3094
  return [
2928
3095
  `BDD feature: ${feature.payload.name}`,
@@ -3010,7 +3177,7 @@ function createBddHandler(deps = {}) {
3010
3177
  const [subcommand, ...restArgs] = request.args;
3011
3178
  const parsed = parseArgs2(restArgs);
3012
3179
  const requestedFormat = (parsed.flags["--format"] ?? "").trim().toLowerCase();
3013
- const outputFormat = parsed.boolFlags.has("--json") ? "json" : requestedFormat || "text";
3180
+ const outputFormat = parsed.boolFlags.has("--json") ? "json" : parsed.boolFlags.has("--feature") ? "feature" : requestedFormat || "text";
3014
3181
  const config = resolveCliConfig(pickConfigArgs2(parsed), env, cwd);
3015
3182
  const projectKey = config.values.projectKey;
3016
3183
  const issueKey = config.values.issueKey;
@@ -3152,10 +3319,10 @@ function createBddHandler(deps = {}) {
3152
3319
  };
3153
3320
  }
3154
3321
  if (nested === "export") {
3155
- const scenarioSelector = parsed.flags["--id"]?.trim() ?? "";
3322
+ const scenarioSelector2 = parsed.flags["--id"]?.trim() ?? "";
3156
3323
  const exportAll = parsed.boolFlags.has("--all");
3157
3324
  const outputDirFlag = parsed.flags["--output-dir"]?.trim() ?? "";
3158
- if (!scenarioSelector && !exportAll) {
3325
+ if (!scenarioSelector2 && !exportAll) {
3159
3326
  return {
3160
3327
  exitCode: ExitCode.ValidationError,
3161
3328
  stderr: ["ERROR: Provide --id <SCENARIO_ID|SC-4> or --all for testops bdd scenarios export."]
@@ -3183,12 +3350,12 @@ function createBddHandler(deps = {}) {
3183
3350
  };
3184
3351
  }
3185
3352
  const selectedScenarios = exportAll ? [...scenariosResult.data].sort((left, right) => (left.key ?? left.id).localeCompare(right.key ?? right.id)) : scenariosResult.data.filter(
3186
- (item) => item.id === scenarioSelector || (item.key ?? "").toUpperCase() === scenarioSelector.toUpperCase()
3353
+ (item) => item.id === scenarioSelector2 || (item.key ?? "").toUpperCase() === scenarioSelector2.toUpperCase()
3187
3354
  );
3188
3355
  if (!exportAll && selectedScenarios.length === 0) {
3189
3356
  return {
3190
3357
  exitCode: ExitCode.RemoteError,
3191
- stderr: [`ERROR: NOT_FOUND: BDD scenario ${scenarioSelector} was not found in this project.`]
3358
+ stderr: [`ERROR: NOT_FOUND: BDD scenario ${scenarioSelector2} was not found in this project.`]
3192
3359
  };
3193
3360
  }
3194
3361
  if (selectedScenarios.length === 0) {
@@ -3278,6 +3445,7 @@ function createBddHandler(deps = {}) {
3278
3445
  if (nested === "import") {
3279
3446
  const sourceFile = parsed.flags["--file"] ? import_node_path6.default.resolve(cwd, parsed.flags["--file"]) : "";
3280
3447
  const inputDir = parsed.flags["--input-dir"] ? import_node_path6.default.resolve(cwd, parsed.flags["--input-dir"]) : "";
3448
+ const dryRun = parsed.boolFlags.has("--dry-run");
3281
3449
  if (!sourceFile && !inputDir) {
3282
3450
  return {
3283
3451
  exitCode: ExitCode.ValidationError,
@@ -3318,6 +3486,7 @@ function createBddHandler(deps = {}) {
3318
3486
  };
3319
3487
  }
3320
3488
  const updatedItems = [];
3489
+ const plannedItems = [];
3321
3490
  for (const entry of parsedScenarios) {
3322
3491
  const parsedScenario = entry.parsed;
3323
3492
  const scenario = scenariosResult.data.find(
@@ -3330,6 +3499,19 @@ function createBddHandler(deps = {}) {
3330
3499
  };
3331
3500
  }
3332
3501
  const nextTags = parsedScenario.tags.filter((tag) => !SCENARIO_METADATA_PATTERN.test(tag));
3502
+ if (dryRun) {
3503
+ plannedItems.push({
3504
+ file: entry.filePath,
3505
+ scenarioId: scenario.id,
3506
+ scenarioKey: scenario.key ?? null,
3507
+ featureName: parsedScenario.featureName,
3508
+ scenarioName: parsedScenario.scenarioName,
3509
+ tags: [...nextTags],
3510
+ linkedIssueKeys: [...parsedScenario.linkedIssueKeys],
3511
+ steps: [...parsedScenario.steps]
3512
+ });
3513
+ continue;
3514
+ }
3333
3515
  const updateResult = await context.invokeForgeContract("updateScenario", {
3334
3516
  context: {
3335
3517
  projectKey,
@@ -3362,16 +3544,43 @@ function createBddHandler(deps = {}) {
3362
3544
  action: "bdd-scenarios-import",
3363
3545
  projectKey,
3364
3546
  issueKey: issueKey || null,
3365
- count: updatedItems.length,
3547
+ dryRun,
3548
+ count: dryRun ? plannedItems.length : updatedItems.length,
3366
3549
  file: sourceFile || null,
3367
3550
  inputDir: inputDir || null,
3368
- items: updatedItems.map((entry) => ({
3551
+ items: dryRun ? plannedItems.map((entry) => ({
3552
+ file: entry.file,
3553
+ scenarioId: entry.scenarioId,
3554
+ scenarioKey: entry.scenarioKey,
3555
+ featureName: entry.featureName,
3556
+ scenarioName: entry.scenarioName,
3557
+ tags: [...entry.tags],
3558
+ linkedIssueKeys: [...entry.linkedIssueKeys],
3559
+ steps: entry.steps.map((step) => ({
3560
+ keyword: step.keyword,
3561
+ text: step.text
3562
+ }))
3563
+ })) : updatedItems.map((entry) => ({
3369
3564
  file: entry.file,
3370
3565
  item: toJsonScenario(entry.item)
3371
3566
  }))
3372
3567
  })
3373
3568
  };
3374
3569
  }
3570
+ if (dryRun) {
3571
+ return {
3572
+ exitCode: ExitCode.Success,
3573
+ stdout: plannedItems.flatMap((entry) => [
3574
+ `BDD scenario import dry-run: ${entry.scenarioKey ?? entry.scenarioId}`,
3575
+ `Source: ${entry.file}`,
3576
+ `Feature: ${entry.featureName}`,
3577
+ `Scenario: ${entry.scenarioName}`,
3578
+ `Tags: ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}`,
3579
+ `Linked issues: ${entry.linkedIssueKeys.length > 0 ? entry.linkedIssueKeys.join(", ") : "none"}`,
3580
+ ...entry.steps.map((step) => ` ${step.keyword} ${step.text}`)
3581
+ ])
3582
+ };
3583
+ }
3375
3584
  return {
3376
3585
  exitCode: ExitCode.Success,
3377
3586
  stdout: updatedItems.flatMap((entry) => [
@@ -3387,9 +3596,9 @@ function createBddHandler(deps = {}) {
3387
3596
  };
3388
3597
  }
3389
3598
  }
3390
- const scenarioId2 = parsed.flags["--id"]?.trim() ?? "";
3599
+ const scenarioId = parsed.flags["--id"]?.trim() ?? "";
3391
3600
  const testCaseKey = parsed.flags["--test-case-key"]?.trim().toUpperCase() ?? "";
3392
- if (!scenarioId2 && !testCaseKey) {
3601
+ if (!scenarioId && !testCaseKey) {
3393
3602
  return {
3394
3603
  exitCode: ExitCode.ValidationError,
3395
3604
  stderr: ["ERROR: Provide either --id <SCENARIO_ID> or --test-case-key <TC-197> for testops bdd scenarios run."]
@@ -3397,8 +3606,8 @@ function createBddHandler(deps = {}) {
3397
3606
  }
3398
3607
  try {
3399
3608
  let scenarioIds = [];
3400
- let scenarioLabel = scenarioId2 || testCaseKey;
3401
- if (testCaseKey && !scenarioId2) {
3609
+ let scenarioLabel = scenarioId || testCaseKey;
3610
+ if (testCaseKey && !scenarioId) {
3402
3611
  const testCaseResult = await context.invokeForgeContract("getTestCase", {
3403
3612
  context: {
3404
3613
  projectKey,
@@ -3433,8 +3642,8 @@ function createBddHandler(deps = {}) {
3433
3642
  };
3434
3643
  }
3435
3644
  scenarioLabel = `${testCase.key} (${scenarioIds.length} scenario${scenarioIds.length === 1 ? "" : "s"})`;
3436
- } else if (scenarioId2) {
3437
- scenarioIds = [scenarioId2];
3645
+ } else if (scenarioId) {
3646
+ scenarioIds = [scenarioId];
3438
3647
  }
3439
3648
  const runs = [];
3440
3649
  for (const currentScenarioId of scenarioIds) {
@@ -3490,20 +3699,24 @@ function createBddHandler(deps = {}) {
3490
3699
  };
3491
3700
  }
3492
3701
  }
3493
- const scenarioId = parsed.flags["--id"]?.trim() ?? "";
3494
- if (!scenarioId) {
3702
+ const scenarioSelector = parsed.flags["--id"]?.trim() ?? "";
3703
+ if (!scenarioSelector) {
3495
3704
  return {
3496
3705
  exitCode: ExitCode.ValidationError,
3497
3706
  stderr: ["ERROR: Missing required --id for testops bdd scenarios show."]
3498
3707
  };
3499
3708
  }
3500
3709
  try {
3710
+ const resolvedScenario = await resolveScenarioId(context, projectKey, issueKey, scenarioSelector);
3711
+ if (!resolvedScenario.ok) {
3712
+ return resolvedScenario.response;
3713
+ }
3501
3714
  const result = await context.invokeForgeContract("getBddScenario", {
3502
3715
  context: {
3503
3716
  projectKey,
3504
3717
  issueKey: issueKey || void 0
3505
3718
  },
3506
- scenarioId
3719
+ scenarioId: resolvedScenario.scenarioId
3507
3720
  });
3508
3721
  if (!result.ok) {
3509
3722
  return {
@@ -5412,7 +5625,7 @@ var COMMAND_REGISTRY = [
5412
5625
  {
5413
5626
  name: "config",
5414
5627
  description: "Configuration and auth bootstrap commands",
5415
- subcommands: ["show", "validate"],
5628
+ subcommands: ["show", "validate", "set"],
5416
5629
  handler: createConfigHandler()
5417
5630
  },
5418
5631
  {
@@ -5454,8 +5667,9 @@ var COMMAND_REGISTRY = [
5454
5667
  }
5455
5668
  ];
5456
5669
  function createThinClientContext() {
5457
- const endpoint = process.env.TESTOPS_FORGE_ENDPOINT?.trim() ?? "";
5458
- const authToken = process.env.TESTOPS_FORGE_AUTH_TOKEN?.trim() ?? "";
5670
+ const config = resolveCliConfig([], process.env, process.cwd()).values;
5671
+ const endpoint = process.env.TESTOPS_FORGE_ENDPOINT?.trim() || config.forgeEndpoint;
5672
+ const authToken = process.env.TESTOPS_FORGE_AUTH_TOKEN?.trim() || config.forgeAuthToken;
5459
5673
  if (!endpoint) {
5460
5674
  return {
5461
5675
  invokeForgeContract: async (_contractName, _payload) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automatify-au/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Forge-first CLI for Automatify Jira TestOps",
5
5
  "type": "module",
6
6
  "bin": {