@automatify-au/cli 0.1.14 → 0.1.16

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 +17 -3
  2. package/dist/automatify.cjs +1301 -257
  3. package/package.json +1 -1
@@ -10765,10 +10765,170 @@ var Priority;
10765
10765
 
10766
10766
  // src/config.ts
10767
10767
  var import_node_fs = require("node:fs");
10768
- var import_node_child_process = require("node:child_process");
10769
10768
  var import_node_path = __toESM(require("node:path"), 1);
10770
- var DEFAULT_CONFIG_FILENAME = ".testops-cli.json";
10769
+
10770
+ // src/secureStore.ts
10771
+ var import_node_child_process = require("node:child_process");
10771
10772
  var KEYCHAIN_SERVICE = "automatify-testops-cli";
10773
+ function errorText(error) {
10774
+ return error instanceof Error && error.message ? error.message : "unknown secure-storage error";
10775
+ }
10776
+ function windowsProtectScript() {
10777
+ return [
10778
+ "Add-Type -AssemblyName System.Security",
10779
+ "$plain = [Console]::In.ReadToEnd()",
10780
+ "$bytes = [Text.Encoding]::UTF8.GetBytes($plain)",
10781
+ "$cipher = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
10782
+ "[Console]::Out.Write([Convert]::ToBase64String($cipher))"
10783
+ ].join("; ");
10784
+ }
10785
+ function windowsUnprotectScript() {
10786
+ return [
10787
+ "Add-Type -AssemblyName System.Security",
10788
+ "$encoded = [Console]::In.ReadToEnd()",
10789
+ "$cipher = [Convert]::FromBase64String($encoded)",
10790
+ "$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($cipher, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
10791
+ "[Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))"
10792
+ ].join("; ");
10793
+ }
10794
+ function quoteSecurityInteractiveArg(value) {
10795
+ if (/\r|\n|\0/.test(value)) {
10796
+ throw new Error("macOS Keychain values cannot contain newline or NUL characters.");
10797
+ }
10798
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
10799
+ }
10800
+ function macOSKeychainWriteInput(account, value) {
10801
+ return [
10802
+ "add-generic-password",
10803
+ "-U",
10804
+ "-s",
10805
+ quoteSecurityInteractiveArg(KEYCHAIN_SERVICE),
10806
+ "-a",
10807
+ quoteSecurityInteractiveArg(account),
10808
+ "-w",
10809
+ quoteSecurityInteractiveArg(value)
10810
+ ].join(" ") + "\n";
10811
+ }
10812
+ function readSecureSecret(locator, deps = {}) {
10813
+ const platform = deps.platform ?? process.platform;
10814
+ const run = deps.execFileSync ?? import_node_child_process.execFileSync;
10815
+ if (platform === "darwin") {
10816
+ if (!locator.account) {
10817
+ return locator.protectedValue ? {
10818
+ status: "error",
10819
+ message: "This config contains a Windows DPAPI value, but it is being read on macOS. Re-store the secret on this machine."
10820
+ } : { status: "missing" };
10821
+ }
10822
+ try {
10823
+ const value = run("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", locator.account, "-w"], {
10824
+ encoding: "utf8",
10825
+ stdio: ["ignore", "pipe", "pipe"]
10826
+ }).trim();
10827
+ return value ? { status: "available", value, source: "keychain" } : {
10828
+ status: "error",
10829
+ message: `macOS Keychain returned an empty value for account ${locator.account}. Re-store the secret.`
10830
+ };
10831
+ } catch (error) {
10832
+ return {
10833
+ status: "error",
10834
+ message: `Unable to read macOS Keychain account ${locator.account}: ${errorText(error)}. Re-store the secret.`
10835
+ };
10836
+ }
10837
+ }
10838
+ if (platform === "win32") {
10839
+ if (!locator.protectedValue) {
10840
+ return locator.account ? {
10841
+ status: "error",
10842
+ message: "This config references a macOS Keychain secret, but it is being read on Windows. Re-store the secret for the current Windows user."
10843
+ } : { status: "missing" };
10844
+ }
10845
+ try {
10846
+ const value = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", windowsUnprotectScript()], {
10847
+ input: locator.protectedValue,
10848
+ encoding: "utf8",
10849
+ stdio: ["pipe", "pipe", "pipe"]
10850
+ }).trim();
10851
+ return value ? { status: "available", value, source: "secure-store" } : {
10852
+ status: "error",
10853
+ message: "Windows DPAPI returned an empty value. Re-store the secret for the current Windows user."
10854
+ };
10855
+ } catch (error) {
10856
+ return {
10857
+ status: "error",
10858
+ message: `Unable to decrypt the Windows DPAPI value for the current user: ${errorText(error)}. Re-store the secret.`
10859
+ };
10860
+ }
10861
+ }
10862
+ if (locator.account || locator.protectedValue) {
10863
+ return {
10864
+ status: "error",
10865
+ message: "This config contains local secure-store metadata, but local secure storage is supported only on macOS and Windows. Use an environment variable on Linux/CI."
10866
+ };
10867
+ }
10868
+ return { status: "missing" };
10869
+ }
10870
+ function writeSecureSecret(account, value, deps = {}) {
10871
+ const platform = deps.platform ?? process.platform;
10872
+ const run = deps.execFileSync ?? import_node_child_process.execFileSync;
10873
+ if (platform === "darwin") {
10874
+ try {
10875
+ run("security", ["-q", "-i"], {
10876
+ input: macOSKeychainWriteInput(account, value),
10877
+ encoding: "utf8",
10878
+ stdio: ["pipe", "ignore", "pipe"]
10879
+ });
10880
+ } catch {
10881
+ throw new Error("Unable to store the secret in macOS Keychain.");
10882
+ }
10883
+ return { source: "keychain", account };
10884
+ }
10885
+ if (platform === "win32") {
10886
+ let protectedValue;
10887
+ try {
10888
+ protectedValue = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", windowsProtectScript()], {
10889
+ input: value,
10890
+ encoding: "utf8",
10891
+ stdio: ["pipe", "pipe", "pipe"]
10892
+ }).trim();
10893
+ } catch {
10894
+ throw new Error("Unable to protect the secret with Windows DPAPI for the current user.");
10895
+ }
10896
+ if (!protectedValue) {
10897
+ throw new Error("Windows DPAPI returned an empty protected value.");
10898
+ }
10899
+ return { source: "secure-store", protectedValue };
10900
+ }
10901
+ throw new Error(
10902
+ "Secure local secret storage is supported on macOS and Windows only. On Linux/CI, keep secrets in environment variables."
10903
+ );
10904
+ }
10905
+ function deleteSecureSecret(locator, deps = {}) {
10906
+ const platform = deps.platform ?? process.platform;
10907
+ const run = deps.execFileSync ?? import_node_child_process.execFileSync;
10908
+ if (platform === "darwin") {
10909
+ if (!locator.account) {
10910
+ return { status: "missing" };
10911
+ }
10912
+ try {
10913
+ run("security", ["delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", locator.account], {
10914
+ stdio: ["ignore", "ignore", "pipe"]
10915
+ });
10916
+ return { status: "deleted" };
10917
+ } catch (error) {
10918
+ return {
10919
+ status: "error",
10920
+ message: `Unable to delete macOS Keychain account ${locator.account}: ${errorText(error)}.`
10921
+ };
10922
+ }
10923
+ }
10924
+ if (platform === "win32") {
10925
+ return locator.protectedValue ? { status: "deleted" } : { status: "missing" };
10926
+ }
10927
+ return locator.account || locator.protectedValue ? { status: "deleted" } : { status: "missing" };
10928
+ }
10929
+
10930
+ // src/config.ts
10931
+ var DEFAULT_CONFIG_FILENAME = ".testops-cli.json";
10772
10932
  function parseFlags(args) {
10773
10933
  const flags = {};
10774
10934
  const unknownFlags = [];
@@ -10802,6 +10962,15 @@ function parseFlags(args) {
10802
10962
  function toStringValue(value) {
10803
10963
  return typeof value === "string" ? value.trim() : "";
10804
10964
  }
10965
+ function firstStringValue(...values) {
10966
+ for (const value of values) {
10967
+ const normalized = toStringValue(value);
10968
+ if (normalized) {
10969
+ return normalized;
10970
+ }
10971
+ }
10972
+ return "";
10973
+ }
10805
10974
  function readConfigFile(configPath) {
10806
10975
  if (!(0, import_node_fs.existsSync)(configPath)) {
10807
10976
  return {};
@@ -10814,6 +10983,27 @@ function readConfigFile(configPath) {
10814
10983
  return {};
10815
10984
  }
10816
10985
  }
10986
+ function readConfigFileForMutation(configPath) {
10987
+ if (!(0, import_node_fs.existsSync)(configPath)) {
10988
+ return { file: {} };
10989
+ }
10990
+ try {
10991
+ const raw = (0, import_node_fs.readFileSync)(configPath, "utf8");
10992
+ const parsed = JSON.parse(raw);
10993
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10994
+ return {
10995
+ file: {},
10996
+ error: `Unable to update config at ${configPath}: existing config must contain a JSON object.`
10997
+ };
10998
+ }
10999
+ return { file: parsed };
11000
+ } catch {
11001
+ return {
11002
+ file: {},
11003
+ error: `Unable to update config at ${configPath}: existing config is not valid JSON. Fix or remove it before retrying.`
11004
+ };
11005
+ }
11006
+ }
10817
11007
  function writeConfigFile(configPath, file) {
10818
11008
  (0, import_node_fs.writeFileSync)(configPath, `${JSON.stringify(file, null, 2)}
10819
11009
  `, "utf8");
@@ -10833,63 +11023,46 @@ function readAuthMode(input) {
10833
11023
  function defaultKeychainAccount(configPath) {
10834
11024
  return import_node_path.default.resolve(configPath);
10835
11025
  }
10836
- function readKeychainSecret(account) {
10837
- if (!account || process.platform !== "darwin") {
10838
- return "";
11026
+ function defaultJiraApiTokenKeychainAccount(configPath) {
11027
+ return `${import_node_path.default.resolve(configPath)}:jiraApiToken`;
11028
+ }
11029
+ function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "") {
11030
+ if (flagValue) {
11031
+ return { value: flagValue, source: "flag" };
10839
11032
  }
10840
- try {
10841
- return (0, import_node_child_process.execFileSync)("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"], {
10842
- encoding: "utf8",
10843
- stdio: ["ignore", "pipe", "ignore"]
10844
- }).trim();
10845
- } catch {
10846
- return "";
11033
+ if (envValue) {
11034
+ return { value: envValue, source: "env" };
10847
11035
  }
10848
- }
10849
- function setKeychainSecret(account, value) {
10850
- if (process.platform !== "darwin") {
10851
- throw new Error("Keychain-backed secrets are only supported on macOS.");
11036
+ if (fileValue) {
11037
+ return { value: fileValue, source: "file" };
10852
11038
  }
10853
- (0, import_node_child_process.execFileSync)("security", ["add-generic-password", "-U", "-s", KEYCHAIN_SERVICE, "-a", account, "-w", value], {
10854
- stdio: ["ignore", "ignore", "pipe"]
10855
- });
11039
+ if (fallback) {
11040
+ return { value: fallback, source: "default" };
11041
+ }
11042
+ return { value: "", source: "default" };
10856
11043
  }
10857
- function protectWindowsSecret(value) {
10858
- const script = [
10859
- "Add-Type -AssemblyName System.Security",
10860
- "$plain = [Console]::In.ReadToEnd()",
10861
- "$bytes = [Text.Encoding]::UTF8.GetBytes($plain)",
10862
- "$cipher = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
10863
- "[Console]::Out.Write([Convert]::ToBase64String($cipher))"
10864
- ].join("; ");
10865
- return (0, import_node_child_process.execFileSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
10866
- input: value,
10867
- encoding: "utf8",
10868
- stdio: ["pipe", "pipe", "pipe"]
10869
- }).trim();
10870
- }
10871
- function unprotectWindowsSecret(protectedValue) {
10872
- if (!protectedValue || process.platform !== "win32") {
10873
- return "";
11044
+ function secureAccountForKey(key, file, configPath) {
11045
+ if (key === "forgeAuthToken") {
11046
+ return toStringValue(file.forgeAuthTokenKeychainAccount) || defaultKeychainAccount(configPath);
10874
11047
  }
10875
- const script = [
10876
- "Add-Type -AssemblyName System.Security",
10877
- "$encoded = [Console]::In.ReadToEnd()",
10878
- "$cipher = [Convert]::FromBase64String($encoded)",
10879
- "$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($cipher, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
10880
- "[Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))"
10881
- ].join("; ");
10882
- try {
10883
- return (0, import_node_child_process.execFileSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
10884
- input: protectedValue,
10885
- encoding: "utf8",
10886
- stdio: ["pipe", "pipe", "ignore"]
10887
- }).trim();
10888
- } catch {
10889
- return "";
11048
+ return toStringValue(file.jiraApiTokenKeychainAccount) || defaultJiraApiTokenKeychainAccount(configPath);
11049
+ }
11050
+ function secureLocatorForKey(key, file) {
11051
+ if (key === "forgeAuthToken") {
11052
+ return {
11053
+ account: toStringValue(file.forgeAuthTokenKeychainAccount) || void 0,
11054
+ protectedValue: toStringValue(file.forgeAuthTokenProtected) || void 0
11055
+ };
10890
11056
  }
11057
+ return {
11058
+ account: toStringValue(file.jiraApiTokenKeychainAccount) || void 0,
11059
+ protectedValue: toStringValue(file.jiraApiTokenProtected) || void 0
11060
+ };
10891
11061
  }
10892
- function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "") {
11062
+ function secretEnvironmentName(key) {
11063
+ return key === "jiraApiToken" ? "JIRA_API_TOKEN" : "TESTOPS_FORGE_AUTH_TOKEN";
11064
+ }
11065
+ function resolveSecretValue(key, flagValue, envValue, fileValue, file) {
10893
11066
  if (flagValue) {
10894
11067
  return { value: flagValue, source: "flag" };
10895
11068
  }
@@ -10899,11 +11072,33 @@ function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "")
10899
11072
  if (fileValue) {
10900
11073
  return { value: fileValue, source: "file" };
10901
11074
  }
10902
- if (fallback) {
10903
- return { value: fallback, source: "default" };
11075
+ const secure = readSecureSecret(secureLocatorForKey(key, file));
11076
+ if (secure.status === "available") {
11077
+ return { value: secure.value, source: secure.source };
11078
+ }
11079
+ if (secure.status === "error") {
11080
+ return { value: "", source: "default", error: `${key}: ${secure.message}` };
10904
11081
  }
10905
11082
  return { value: "", source: "default" };
10906
11083
  }
11084
+ function clearSecretFields(file, key) {
11085
+ if (key === "forgeAuthToken") {
11086
+ return {
11087
+ ...file,
11088
+ forgeAuthToken: void 0,
11089
+ TESTOPS_FORGE_AUTH_TOKEN: void 0,
11090
+ forgeAuthTokenKeychainAccount: void 0,
11091
+ forgeAuthTokenProtected: void 0
11092
+ };
11093
+ }
11094
+ return {
11095
+ ...file,
11096
+ jiraApiToken: void 0,
11097
+ JIRA_API_TOKEN: void 0,
11098
+ jiraApiTokenKeychainAccount: void 0,
11099
+ jiraApiTokenProtected: void 0
11100
+ };
11101
+ }
10907
11102
  function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
10908
11103
  const { flags, unknownFlags } = parseFlags(args);
10909
11104
  const configPathFlag = flags["--config"] ? import_node_path.default.resolve(cwd, flags["--config"]) : "";
@@ -10942,11 +11137,12 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
10942
11137
  toStringValue(env.JIRA_EMAIL),
10943
11138
  toStringValue(file.jiraEmail ?? file.JIRA_EMAIL)
10944
11139
  );
10945
- const jiraApiTokenResolved = valueFromPrecedence(
11140
+ const jiraApiTokenResolved = resolveSecretValue(
10946
11141
  "jiraApiToken",
10947
11142
  toStringValue(flags["--jira-api-token"]),
10948
11143
  toStringValue(env.JIRA_API_TOKEN),
10949
- toStringValue(file.jiraApiToken ?? file.JIRA_API_TOKEN)
11144
+ firstStringValue(file.jiraApiToken, file.JIRA_API_TOKEN),
11145
+ file
10950
11146
  );
10951
11147
  const forgeEndpointResolved = valueFromPrecedence(
10952
11148
  "forgeEndpoint",
@@ -10960,11 +11156,13 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
10960
11156
  "",
10961
11157
  toStringValue(file.forgeAuthTokenKeychainAccount)
10962
11158
  );
10963
- const forgeAuthTokenFromEnv = toStringValue(env.TESTOPS_FORGE_AUTH_TOKEN);
10964
- const forgeAuthTokenFromFile = toStringValue(file.forgeAuthToken ?? file.TESTOPS_FORGE_AUTH_TOKEN);
10965
- const forgeAuthTokenFromKeychain = forgeAuthTokenFromEnv || forgeAuthTokenFromFile || !forgeAuthTokenKeychainAccountResolved.value ? "" : readKeychainSecret(forgeAuthTokenKeychainAccountResolved.value);
10966
- const forgeAuthTokenFromWindowsStore = forgeAuthTokenFromEnv || forgeAuthTokenFromFile || forgeAuthTokenFromKeychain ? "" : unprotectWindowsSecret(toStringValue(file.forgeAuthTokenProtected));
10967
- const forgeAuthTokenResolved = forgeAuthTokenFromEnv ? { value: forgeAuthTokenFromEnv, source: "env" } : forgeAuthTokenFromFile ? { value: forgeAuthTokenFromFile, source: "file" } : forgeAuthTokenFromKeychain ? { value: forgeAuthTokenFromKeychain, source: "keychain" } : forgeAuthTokenFromWindowsStore ? { value: forgeAuthTokenFromWindowsStore, source: "secure-store" } : { value: "", source: "default" };
11159
+ const forgeAuthTokenResolved = resolveSecretValue(
11160
+ "forgeAuthToken",
11161
+ "",
11162
+ toStringValue(env.TESTOPS_FORGE_AUTH_TOKEN),
11163
+ firstStringValue(file.forgeAuthToken, file.TESTOPS_FORGE_AUTH_TOKEN),
11164
+ file
11165
+ );
10968
11166
  const config = {
10969
11167
  baseUrl: normalizeBaseUrl(baseUrlResolved.value),
10970
11168
  projectKey: projectKeyResolved.value,
@@ -10990,13 +11188,16 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
10990
11188
  configPath: configPathSource
10991
11189
  };
10992
11190
  const warnings = [];
11191
+ const errors = [jiraApiTokenResolved.error, forgeAuthTokenResolved.error].filter(
11192
+ (value) => Boolean(value)
11193
+ );
10993
11194
  if (unknownFlags.length > 0) {
10994
11195
  warnings.push(`Ignored unknown config flags: ${unknownFlags.join(", ")}`);
10995
11196
  }
10996
11197
  if (!(0, import_node_fs.existsSync)(configPath) && sources.configPath !== "default") {
10997
11198
  warnings.push(`Config file not found at ${configPath}; using env/flags/defaults.`);
10998
11199
  }
10999
- return { values: config, sources, warnings };
11200
+ return { values: config, sources, warnings, errors };
11000
11201
  }
11001
11202
  function maskSecret(value) {
11002
11203
  if (!value) {
@@ -11009,7 +11210,7 @@ function maskSecret(value) {
11009
11210
  }
11010
11211
  function validateResolvedConfig(resolution) {
11011
11212
  const { values } = resolution;
11012
- const errors = [];
11213
+ const errors = [...resolution.errors];
11013
11214
  const warnings = [...resolution.warnings];
11014
11215
  if (!values.baseUrl) {
11015
11216
  errors.push("Missing required setting: JIRA_BASE_URL (flag/env/file).");
@@ -11024,10 +11225,8 @@ function validateResolvedConfig(resolution) {
11024
11225
  if (!values.jiraApiToken) {
11025
11226
  errors.push("Auth mode api-token requires JIRA_API_TOKEN.");
11026
11227
  }
11027
- } else {
11028
- if (values.jiraApiToken || values.jiraEmail) {
11029
- warnings.push("JIRA_EMAIL/JIRA_API_TOKEN were provided but auth mode is 'none'.");
11030
- }
11228
+ } else if (values.jiraApiToken || values.jiraEmail) {
11229
+ warnings.push("JIRA_EMAIL/JIRA_API_TOKEN were provided but auth mode is 'none'.");
11031
11230
  }
11032
11231
  return { ok: errors.length === 0, errors, warnings };
11033
11232
  }
@@ -11090,53 +11289,33 @@ function applyConfigSet(configPath, rawKey, value) {
11090
11289
  ]
11091
11290
  };
11092
11291
  }
11093
- const file = readConfigFile(configPath);
11094
- if (key === "forgeAuthToken") {
11095
- if (process.platform === "win32") {
11096
- try {
11097
- const protectedValue = protectWindowsSecret(value);
11098
- writeConfigFile(configPath, {
11099
- ...file,
11100
- forgeAuthToken: void 0,
11101
- TESTOPS_FORGE_AUTH_TOKEN: void 0,
11102
- forgeAuthTokenKeychainAccount: void 0,
11103
- forgeAuthTokenProtected: protectedValue
11104
- });
11105
- return {
11106
- exitCode: ExitCode.Success,
11107
- stdout: ["Config updated: forgeAuthToken protected with Windows DPAPI for the current user."]
11108
- };
11109
- } catch (error) {
11110
- return {
11111
- exitCode: ExitCode.ValidationError,
11112
- stderr: [
11113
- `Unable to store forgeAuthToken securely: ${error instanceof Error ? error.message : "unknown secure-storage error"}`
11114
- ]
11115
- };
11116
- }
11117
- }
11118
- if (process.platform !== "darwin") {
11292
+ const mutable = readConfigFileForMutation(configPath);
11293
+ if (mutable.error) {
11294
+ return { exitCode: ExitCode.ValidationError, stderr: [mutable.error] };
11295
+ }
11296
+ const file = mutable.file;
11297
+ if (key === "forgeAuthToken" || key === "jiraApiToken") {
11298
+ const secretKey = key;
11299
+ const account = secureAccountForKey(secretKey, file, configPath);
11300
+ try {
11301
+ const stored = writeSecureSecret(account, value);
11302
+ const cleared = clearSecretFields(file, secretKey);
11303
+ const nextFile2 = stored.source === "keychain" ? secretKey === "forgeAuthToken" ? { ...cleared, forgeAuthTokenKeychainAccount: stored.account } : { ...cleared, jiraApiTokenKeychainAccount: stored.account } : secretKey === "forgeAuthToken" ? { ...cleared, forgeAuthTokenProtected: stored.protectedValue } : { ...cleared, jiraApiTokenProtected: stored.protectedValue };
11304
+ writeConfigFile(configPath, nextFile2);
11305
+ const backend = stored.source === "keychain" ? "macOS Keychain" : "Windows DPAPI for the current user";
11119
11306
  return {
11120
- exitCode: ExitCode.UsageError,
11307
+ exitCode: ExitCode.Success,
11308
+ stdout: [`Config updated: ${secretKey} stored securely with ${backend}.`]
11309
+ };
11310
+ } catch (error) {
11311
+ const unsupported = process.platform !== "darwin" && process.platform !== "win32";
11312
+ return {
11313
+ exitCode: unsupported ? ExitCode.UsageError : ExitCode.ValidationError,
11121
11314
  stderr: [
11122
- "Secure local forgeAuthToken storage is currently supported on macOS and Windows. On Linux/CI, set TESTOPS_FORGE_AUTH_TOKEN in the environment."
11315
+ `Unable to store ${secretKey} securely: ${error instanceof Error ? error.message : "unknown secure-storage error"} Use ${secretEnvironmentName(secretKey)} on Linux/CI.`
11123
11316
  ]
11124
11317
  };
11125
11318
  }
11126
- const account = toStringValue(file.forgeAuthTokenKeychainAccount) || defaultKeychainAccount(configPath);
11127
- setKeychainSecret(account, value);
11128
- const nextFile2 = {
11129
- ...file,
11130
- forgeAuthToken: void 0,
11131
- TESTOPS_FORGE_AUTH_TOKEN: void 0,
11132
- forgeAuthTokenProtected: void 0,
11133
- forgeAuthTokenKeychainAccount: account
11134
- };
11135
- writeConfigFile(configPath, nextFile2);
11136
- return {
11137
- exitCode: ExitCode.Success,
11138
- stdout: [`Config updated: forgeAuthToken stored in macOS Keychain (${account}).`]
11139
- };
11140
11319
  }
11141
11320
  const nextFile = {
11142
11321
  ...file,
@@ -11148,6 +11327,101 @@ function applyConfigSet(configPath, rawKey, value) {
11148
11327
  stdout: [`Config updated: ${rawKey}.`]
11149
11328
  };
11150
11329
  }
11330
+ function configFileKeysFor(key) {
11331
+ switch (key) {
11332
+ case "baseUrl":
11333
+ return ["baseUrl", "JIRA_BASE_URL"];
11334
+ case "projectKey":
11335
+ return ["projectKey", "JIRA_PROJECT_KEY"];
11336
+ case "issueKey":
11337
+ return ["issueKey", "JIRA_ISSUE_KEY"];
11338
+ case "authMode":
11339
+ return ["authMode", "TESTOPS_AUTH_MODE"];
11340
+ case "jiraEmail":
11341
+ return ["jiraEmail", "JIRA_EMAIL"];
11342
+ case "forgeEndpoint":
11343
+ return ["forgeEndpoint", "TESTOPS_FORGE_ENDPOINT"];
11344
+ default:
11345
+ return [key];
11346
+ }
11347
+ }
11348
+ function applyConfigUnset(configPath, rawKey) {
11349
+ const key = normalizeConfigSetKey(rawKey);
11350
+ if (!key) {
11351
+ return {
11352
+ exitCode: ExitCode.UsageError,
11353
+ stderr: ["Usage: automatify testops config unset <key>"]
11354
+ };
11355
+ }
11356
+ const mutable = readConfigFileForMutation(configPath);
11357
+ if (mutable.error) {
11358
+ return { exitCode: ExitCode.ValidationError, stderr: [mutable.error] };
11359
+ }
11360
+ const file = mutable.file;
11361
+ if (key === "forgeAuthToken" || key === "jiraApiToken") {
11362
+ const secretKey = key;
11363
+ const deleted = deleteSecureSecret(secureLocatorForKey(secretKey, file));
11364
+ if (deleted.status === "error") {
11365
+ return { exitCode: ExitCode.ValidationError, stderr: [deleted.message] };
11366
+ }
11367
+ writeConfigFile(configPath, clearSecretFields(file, secretKey));
11368
+ return { exitCode: ExitCode.Success, stdout: [`Config removed: ${secretKey}.`] };
11369
+ }
11370
+ const next = { ...file };
11371
+ for (const fileKey of configFileKeysFor(key)) {
11372
+ delete next[fileKey];
11373
+ }
11374
+ writeConfigFile(configPath, next);
11375
+ return { exitCode: ExitCode.Success, stdout: [`Config removed: ${rawKey}.`] };
11376
+ }
11377
+ function applyConfigMigrateSecrets(configPath) {
11378
+ const mutable = readConfigFileForMutation(configPath);
11379
+ if (mutable.error) {
11380
+ return { exitCode: ExitCode.ValidationError, stderr: [mutable.error] };
11381
+ }
11382
+ const file = mutable.file;
11383
+ const pending = [];
11384
+ const jiraApiToken = firstStringValue(file.jiraApiToken, file.JIRA_API_TOKEN);
11385
+ const forgeAuthToken = firstStringValue(file.forgeAuthToken, file.TESTOPS_FORGE_AUTH_TOKEN);
11386
+ if (jiraApiToken) {
11387
+ pending.push({ key: "jiraApiToken", value: jiraApiToken });
11388
+ }
11389
+ if (forgeAuthToken) {
11390
+ pending.push({ key: "forgeAuthToken", value: forgeAuthToken });
11391
+ }
11392
+ if (pending.length === 0) {
11393
+ return { exitCode: ExitCode.Success, stdout: ["No plaintext local secrets require migration."] };
11394
+ }
11395
+ const migrated = [];
11396
+ for (const item of pending) {
11397
+ const response = applyConfigSet(configPath, item.key, item.value);
11398
+ if (response.exitCode !== ExitCode.Success) {
11399
+ return {
11400
+ exitCode: response.exitCode,
11401
+ stdout: migrated.length > 0 ? [`Migrated before failure: ${migrated.join(", ")}.`] : void 0,
11402
+ stderr: response.stderr
11403
+ };
11404
+ }
11405
+ migrated.push(item.key);
11406
+ }
11407
+ return {
11408
+ exitCode: ExitCode.Success,
11409
+ stdout: [`Migrated plaintext secrets to secure storage: ${migrated.join(", ")}.`]
11410
+ };
11411
+ }
11412
+ function stripConfigPathArgs(args) {
11413
+ const operands = [];
11414
+ for (let i = 0; i < args.length; i += 1) {
11415
+ if (args[i] === "--config") {
11416
+ if (args[i + 1] && !args[i + 1].startsWith("--")) {
11417
+ i += 1;
11418
+ }
11419
+ continue;
11420
+ }
11421
+ operands.push(args[i]);
11422
+ }
11423
+ return operands;
11424
+ }
11151
11425
  function createConfigHandler(env = process.env, cwd = process.cwd()) {
11152
11426
  return (request) => {
11153
11427
  const [subcommand, ...subArgs] = request.args;
@@ -11156,7 +11430,11 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
11156
11430
  if (subcommand === "show") {
11157
11431
  return {
11158
11432
  exitCode: ExitCode.Success,
11159
- stdout: [...toDisplayLines(resolution), ...validation.warnings.map((line) => `WARN: ${line}`)]
11433
+ stdout: [
11434
+ ...toDisplayLines(resolution),
11435
+ ...validation.warnings.map((line) => `WARN: ${line}`),
11436
+ ...resolution.errors.map((line) => `ERROR: ${line}`)
11437
+ ]
11160
11438
  };
11161
11439
  }
11162
11440
  if (subcommand === "validate") {
@@ -11177,17 +11455,25 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
11177
11455
  };
11178
11456
  }
11179
11457
  if (subcommand === "set") {
11180
- const [key, ...valueParts] = subArgs;
11181
- const readValueFromStdin = key === "forgeAuthToken" && valueParts.length === 1 && valueParts[0] === "--stdin";
11458
+ const [key, ...valueParts] = stripConfigPathArgs(subArgs);
11459
+ const normalizedKey = normalizeConfigSetKey(key ?? "");
11460
+ const readValueFromStdin = (normalizedKey === "forgeAuthToken" || normalizedKey === "jiraApiToken") && valueParts.length === 1 && valueParts[0] === "--stdin";
11182
11461
  const value = readValueFromStdin ? (0, import_node_fs.readFileSync)(0, "utf8").trim() : valueParts.join(" ").trim();
11183
11462
  if (!key || !value) {
11184
11463
  return {
11185
11464
  exitCode: ExitCode.UsageError,
11186
- stderr: ["Usage: automatify testops config set <key> <value>"]
11465
+ stderr: ["Usage: automatify testops config set <key> <value|--stdin>"]
11187
11466
  };
11188
11467
  }
11189
11468
  return applyConfigSet(resolution.values.configPath, key, value);
11190
11469
  }
11470
+ if (subcommand === "unset") {
11471
+ const [key] = stripConfigPathArgs(subArgs);
11472
+ return applyConfigUnset(resolution.values.configPath, key ?? "");
11473
+ }
11474
+ if (subcommand === "migrate-secrets") {
11475
+ return applyConfigMigrateSecrets(resolution.values.configPath);
11476
+ }
11191
11477
  return {
11192
11478
  exitCode: ExitCode.UsageError,
11193
11479
  stderr: [`Unsupported config subcommand: ${subcommand}`]
@@ -13627,33 +13913,280 @@ function createAllureHandler(deps = {}) {
13627
13913
  };
13628
13914
  }
13629
13915
 
13630
- // src/bdd.ts
13916
+ // src/auth.ts
13631
13917
  var import_node_fs10 = require("node:fs");
13632
- var import_node_path11 = __toESM(require("node:path"), 1);
13633
- var import_yazl = __toESM(require_yazl(), 1);
13634
-
13635
- // src/forgeClient.ts
13636
- var ForgeClientError = class extends Error {
13637
- code;
13638
- status;
13639
- retryable;
13640
- constructor(input) {
13641
- super(input.message);
13642
- this.name = "ForgeClientError";
13643
- this.code = input.code;
13644
- this.status = input.status;
13645
- this.retryable = Boolean(input.retryable);
13918
+ function parseAuthArgs(args, allowLoginFlags) {
13919
+ const configArgs = [];
13920
+ const unknown = [];
13921
+ let baseUrl = "";
13922
+ let jiraEmail = "";
13923
+ let tokenStdin = false;
13924
+ for (let i = 0; i < args.length; i += 1) {
13925
+ const token = args[i];
13926
+ if (token === "--config") {
13927
+ const value = args[i + 1];
13928
+ if (!value || value.startsWith("--")) {
13929
+ unknown.push(token);
13930
+ continue;
13931
+ }
13932
+ configArgs.push(token, value);
13933
+ i += 1;
13934
+ continue;
13935
+ }
13936
+ if (allowLoginFlags && token === "--base-url") {
13937
+ const value = args[i + 1];
13938
+ if (!value || value.startsWith("--")) {
13939
+ unknown.push(token);
13940
+ continue;
13941
+ }
13942
+ baseUrl = value.trim();
13943
+ i += 1;
13944
+ continue;
13945
+ }
13946
+ if (allowLoginFlags && token === "--jira-email") {
13947
+ const value = args[i + 1];
13948
+ if (!value || value.startsWith("--")) {
13949
+ unknown.push(token);
13950
+ continue;
13951
+ }
13952
+ jiraEmail = value.trim();
13953
+ i += 1;
13954
+ continue;
13955
+ }
13956
+ if (allowLoginFlags && token === "--token-stdin") {
13957
+ tokenStdin = true;
13958
+ continue;
13959
+ }
13960
+ unknown.push(token);
13646
13961
  }
13647
- };
13648
- function isObject(value) {
13649
- return Boolean(value) && typeof value === "object";
13962
+ return { configArgs, baseUrl, jiraEmail, tokenStdin, unknown };
13650
13963
  }
13651
- function isServiceError(value) {
13652
- if (!isObject(value)) {
13653
- return false;
13964
+ function runConfigMutation(handler, args, context) {
13965
+ const response = handler(
13966
+ {
13967
+ group: "config",
13968
+ args,
13969
+ rawArgs: ["config", ...args]
13970
+ },
13971
+ context
13972
+ );
13973
+ if (response instanceof Promise) {
13974
+ throw new Error("Config mutations must remain synchronous.");
13975
+ }
13976
+ return response;
13977
+ }
13978
+ function sourceLabel(source) {
13979
+ switch (source) {
13980
+ case "keychain":
13981
+ return "macOS Keychain";
13982
+ case "secure-store":
13983
+ return "Windows DPAPI";
13984
+ case "env":
13985
+ return "environment";
13986
+ case "file":
13987
+ return "legacy plaintext config";
13988
+ case "flag":
13989
+ return "command flag";
13990
+ default:
13991
+ return "not configured";
13654
13992
  }
13655
- const code = value.code;
13656
- const message = value.message;
13993
+ }
13994
+ function authState(parts) {
13995
+ const populated = parts.filter(Boolean).length;
13996
+ if (populated === 0) {
13997
+ return "not configured";
13998
+ }
13999
+ return populated === parts.length ? "configured" : "incomplete";
14000
+ }
14001
+ function createStatusResponse(args, env, cwd) {
14002
+ const parsed = parseAuthArgs(args, false);
14003
+ if (parsed.unknown.length > 0) {
14004
+ return {
14005
+ exitCode: ExitCode.UsageError,
14006
+ stderr: ["Unsupported auth status arguments. Only --config <path> is supported."]
14007
+ };
14008
+ }
14009
+ const resolution = resolveCliConfig(parsed.configArgs, env, cwd);
14010
+ const { values, sources } = resolution;
14011
+ const jiraState = authState([values.baseUrl, values.jiraEmail, values.jiraApiToken]);
14012
+ const forgeState = authState([values.forgeEndpoint, values.forgeAuthToken]);
14013
+ const stdout = [
14014
+ "Auth status (secret-safe):",
14015
+ ` Jira: ${jiraState}`,
14016
+ ` baseUrl: ${values.baseUrl || "<unset>"}`,
14017
+ ` email: ${values.jiraEmail || "<unset>"}`,
14018
+ ` apiToken: ${values.jiraApiToken ? `configured (${sourceLabel(sources.jiraApiToken)})` : "<unset>"}`,
14019
+ ` Forge transport: ${forgeState}`,
14020
+ ` endpoint: ${values.forgeEndpoint || "<unset>"}`,
14021
+ ` authToken: ${values.forgeAuthToken ? `configured (${sourceLabel(sources.forgeAuthToken)})` : "<unset>"}`,
14022
+ ` configPath: ${values.configPath}`
14023
+ ];
14024
+ if (resolution.errors.length > 0) {
14025
+ return {
14026
+ exitCode: ExitCode.ValidationError,
14027
+ stdout,
14028
+ stderr: resolution.errors.map((error) => `ERROR: ${error}`)
14029
+ };
14030
+ }
14031
+ return { exitCode: ExitCode.Success, stdout };
14032
+ }
14033
+ function createLoginResponse(args, env, cwd, readStdin, platform, context) {
14034
+ const parsed = parseAuthArgs(args, true);
14035
+ if (parsed.unknown.length > 0) {
14036
+ return {
14037
+ exitCode: ExitCode.UsageError,
14038
+ stderr: [
14039
+ "Unsupported auth login arguments. Use --base-url, --jira-email, --token-stdin, and optional --config only.",
14040
+ "Secrets must be provided through --token-stdin or environment variables, never as command-line values."
14041
+ ]
14042
+ };
14043
+ }
14044
+ const current = resolveCliConfig(parsed.configArgs, env, cwd);
14045
+ const baseUrl = parsed.baseUrl || env.JIRA_BASE_URL?.trim() || current.values.baseUrl;
14046
+ const jiraEmail = parsed.jiraEmail || env.JIRA_EMAIL?.trim() || current.values.jiraEmail;
14047
+ const stdinToken = parsed.tokenStdin ? readStdin().trim() : "";
14048
+ const effectiveToken = stdinToken || env.JIRA_API_TOKEN?.trim() || current.values.jiraApiToken;
14049
+ const missing = [];
14050
+ if (!baseUrl) missing.push("--base-url or JIRA_BASE_URL");
14051
+ if (!jiraEmail) missing.push("--jira-email or JIRA_EMAIL");
14052
+ if (!effectiveToken) missing.push("--token-stdin, JIRA_API_TOKEN, or an existing stored token");
14053
+ if (missing.length > 0) {
14054
+ return {
14055
+ exitCode: ExitCode.UsageError,
14056
+ stderr: [
14057
+ `Jira login is incomplete. Provide: ${missing.join(", ")}.`,
14058
+ "Secrets are never accepted as normal command-line values."
14059
+ ]
14060
+ };
14061
+ }
14062
+ if (parsed.tokenStdin && platform !== "darwin" && platform !== "win32") {
14063
+ return {
14064
+ exitCode: ExitCode.UsageError,
14065
+ stderr: [
14066
+ "Local secure secret storage is supported only on macOS and Windows.",
14067
+ "On Linux/CI, set JIRA_API_TOKEN in the environment and run auth login without --token-stdin."
14068
+ ]
14069
+ };
14070
+ }
14071
+ const configHandler = createConfigHandler(env, cwd);
14072
+ const configSuffix = parsed.configArgs;
14073
+ const steps = [];
14074
+ if (stdinToken) {
14075
+ steps.push({ label: "Jira API token", args: ["set", "jiraApiToken", stdinToken, ...configSuffix] });
14076
+ }
14077
+ steps.push(
14078
+ { label: "Jira base URL", args: ["set", "baseUrl", baseUrl, ...configSuffix] },
14079
+ { label: "Jira email", args: ["set", "jiraEmail", jiraEmail, ...configSuffix] },
14080
+ { label: "auth mode", args: ["set", "authMode", "api-token", ...configSuffix] }
14081
+ );
14082
+ const completed = [];
14083
+ for (const step of steps) {
14084
+ const response = runConfigMutation(configHandler, step.args, context);
14085
+ if (response.exitCode !== ExitCode.Success) {
14086
+ return {
14087
+ exitCode: response.exitCode,
14088
+ stdout: completed.length > 0 ? [`Configured before failure: ${completed.join(", ")}.`] : void 0,
14089
+ stderr: response.stderr
14090
+ };
14091
+ }
14092
+ completed.push(step.label);
14093
+ }
14094
+ const tokenSource = stdinToken ? platform === "darwin" ? "macOS Keychain" : "Windows DPAPI" : sourceLabel(
14095
+ current.sources.jiraApiToken === "default" && env.JIRA_API_TOKEN ? "env" : current.sources.jiraApiToken
14096
+ );
14097
+ return {
14098
+ exitCode: ExitCode.Success,
14099
+ stdout: [
14100
+ "Jira login configured.",
14101
+ ` baseUrl: ${baseUrl.replace(/\/$/, "")}`,
14102
+ ` email: ${jiraEmail}`,
14103
+ ` apiToken: configured (${tokenSource})`,
14104
+ "Run `automatify testops auth status` to verify the resolved credentials."
14105
+ ]
14106
+ };
14107
+ }
14108
+ function createLogoutResponse(args, env, cwd, context) {
14109
+ const parsed = parseAuthArgs(args, false);
14110
+ if (parsed.unknown.length > 0) {
14111
+ return {
14112
+ exitCode: ExitCode.UsageError,
14113
+ stderr: ["Unsupported auth logout arguments. Only --config <path> is supported."]
14114
+ };
14115
+ }
14116
+ const configHandler = createConfigHandler(env, cwd);
14117
+ const failures = [];
14118
+ const removed = [];
14119
+ for (const key of ["jiraApiToken", "forgeAuthToken"]) {
14120
+ const response = runConfigMutation(configHandler, ["unset", key, ...parsed.configArgs], context);
14121
+ if (response.exitCode === ExitCode.Success) {
14122
+ removed.push(key);
14123
+ } else {
14124
+ failures.push(...response.stderr ?? [`Unable to remove ${key}.`]);
14125
+ }
14126
+ }
14127
+ if (failures.length > 0) {
14128
+ return {
14129
+ exitCode: ExitCode.ValidationError,
14130
+ stdout: removed.length > 0 ? [`Removed before failure: ${removed.join(", ")}.`] : void 0,
14131
+ stderr: failures
14132
+ };
14133
+ }
14134
+ const stdout = ["Local Jira and Forge credentials removed.", "Non-secret URL/email settings were preserved."];
14135
+ if (env.JIRA_API_TOKEN || env.TESTOPS_FORGE_AUTH_TOKEN) {
14136
+ stdout.push("Environment-provided credentials are unchanged and may still resolve as configured.");
14137
+ }
14138
+ return { exitCode: ExitCode.Success, stdout };
14139
+ }
14140
+ function createAuthHandler(deps = {}) {
14141
+ const env = deps.env ?? process.env;
14142
+ const cwd = deps.cwd ?? process.cwd();
14143
+ const readStdin = deps.readStdin ?? (() => (0, import_node_fs10.readFileSync)(0, "utf8"));
14144
+ const platform = deps.platform ?? process.platform;
14145
+ return (request, context) => {
14146
+ const [subcommand, ...args] = request.args;
14147
+ if (subcommand === "status") {
14148
+ return createStatusResponse(args, env, cwd);
14149
+ }
14150
+ if (subcommand === "login") {
14151
+ return createLoginResponse(args, env, cwd, readStdin, platform, context);
14152
+ }
14153
+ if (subcommand === "logout") {
14154
+ return createLogoutResponse(args, env, cwd, context);
14155
+ }
14156
+ return {
14157
+ exitCode: ExitCode.UsageError,
14158
+ stderr: ["Unsupported auth subcommand. Use login, status, or logout."]
14159
+ };
14160
+ };
14161
+ }
14162
+
14163
+ // src/bdd.ts
14164
+ var import_node_fs11 = require("node:fs");
14165
+ var import_node_path11 = __toESM(require("node:path"), 1);
14166
+ var import_yazl = __toESM(require_yazl(), 1);
14167
+
14168
+ // src/forgeClient.ts
14169
+ var ForgeClientError = class extends Error {
14170
+ code;
14171
+ status;
14172
+ retryable;
14173
+ constructor(input) {
14174
+ super(input.message);
14175
+ this.name = "ForgeClientError";
14176
+ this.code = input.code;
14177
+ this.status = input.status;
14178
+ this.retryable = Boolean(input.retryable);
14179
+ }
14180
+ };
14181
+ function isObject(value) {
14182
+ return Boolean(value) && typeof value === "object";
14183
+ }
14184
+ function isServiceError(value) {
14185
+ if (!isObject(value)) {
14186
+ return false;
14187
+ }
14188
+ const code = value.code;
14189
+ const message = value.message;
13657
14190
  return typeof code === "string" && typeof message === "string";
13658
14191
  }
13659
14192
  function normalizeServiceError(error) {
@@ -14053,7 +14586,7 @@ function toJsonExportManifestItems(items) {
14053
14586
  async function defaultCreateZipArchive(archivePath, entries) {
14054
14587
  await new Promise((resolve, reject) => {
14055
14588
  const zip = new import_yazl.ZipFile();
14056
- const output = zip.outputStream.pipe((0, import_node_fs10.createWriteStream)(archivePath));
14589
+ const output = zip.outputStream.pipe((0, import_node_fs11.createWriteStream)(archivePath));
14057
14590
  output.on("close", () => resolve());
14058
14591
  output.on("error", reject);
14059
14592
  zip.outputStream.on("error", reject);
@@ -14083,10 +14616,10 @@ function missingProjectResponse() {
14083
14616
  function createBddHandler(deps = {}) {
14084
14617
  const cwd = deps.cwd ?? process.cwd();
14085
14618
  const env = deps.env ?? process.env;
14086
- const mkdir = deps.mkdir ?? ((targetPath, options) => (0, import_node_fs10.mkdirSync)(targetPath, options));
14087
- const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs10.readFileSync)(filePath, "utf8"));
14088
- const readDir = deps.readDir ?? ((dirPath) => (0, import_node_fs10.readdirSync)(dirPath));
14089
- const writeFile = deps.writeFile ?? ((filePath, content) => (0, import_node_fs10.writeFileSync)(filePath, content, "utf8"));
14619
+ const mkdir = deps.mkdir ?? ((targetPath, options) => (0, import_node_fs11.mkdirSync)(targetPath, options));
14620
+ const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs11.readFileSync)(filePath, "utf8"));
14621
+ const readDir = deps.readDir ?? ((dirPath) => (0, import_node_fs11.readdirSync)(dirPath));
14622
+ const writeFile = deps.writeFile ?? ((filePath, content) => (0, import_node_fs11.writeFileSync)(filePath, content, "utf8"));
14090
14623
  const createZipArchive = deps.createZipArchive ?? defaultCreateZipArchive;
14091
14624
  return async (request, context) => {
14092
14625
  const [subcommand, ...restArgs] = request.args;
@@ -15179,9 +15712,10 @@ function resolveAllureJiraConfig(parsed, env, cwd) {
15179
15712
  const resolution = resolveCliConfig(pickConfigArgs5(parsed), env, cwd);
15180
15713
  const site = normalizeSite3(parsed.flags["--site"] ?? env.JIRA_SITE ?? resolution.values.baseUrl);
15181
15714
  const email = (parsed.flags["--email"] ?? resolution.values.jiraEmail).trim();
15182
- const apiToken = (parsed.flags["--api-token"] ?? resolution.values.jiraApiToken).trim();
15715
+ const directApiToken = (parsed.flags["--api-token"] ?? "").trim();
15716
+ const apiToken = (directApiToken || resolution.values.jiraApiToken).trim();
15183
15717
  const issueKey = (parsed.flags["--issue-key"] ?? "").trim();
15184
- const errors = [];
15718
+ const errors = directApiToken ? [] : resolution.errors.filter((line) => line.startsWith("jiraApiToken:"));
15185
15719
  if (!hasValue(site)) {
15186
15720
  errors.push("Missing Jira site. Pass --site, set JIRA_SITE/JIRA_BASE_URL, or configure baseUrl.");
15187
15721
  }
@@ -15420,7 +15954,7 @@ function createDoctorHandler(deps = {}) {
15420
15954
  }
15421
15955
 
15422
15956
  // src/ingestFeature.ts
15423
- var import_node_fs11 = require("node:fs");
15957
+ var import_node_fs12 = require("node:fs");
15424
15958
  var import_node_path12 = __toESM(require("node:path"), 1);
15425
15959
  var CONFIG_FLAGS6 = /* @__PURE__ */ new Set([
15426
15960
  "--config",
@@ -15507,8 +16041,8 @@ function normalizeError5(error) {
15507
16041
  return "Unknown ingest error.";
15508
16042
  }
15509
16043
  function createIngestFeatureHandler(deps = {}) {
15510
- const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs11.readFileSync)(filePath, "utf8"));
15511
- const readStdin = deps.readStdin ?? (() => (0, import_node_fs11.readFileSync)(0, "utf8"));
16044
+ const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs12.readFileSync)(filePath, "utf8"));
16045
+ const readStdin = deps.readStdin ?? (() => (0, import_node_fs12.readFileSync)(0, "utf8"));
15512
16046
  const cwd = deps.cwd ?? process.cwd();
15513
16047
  const env = deps.env ?? process.env;
15514
16048
  return async (request, context) => {
@@ -15571,75 +16105,573 @@ function createIngestFeatureHandler(deps = {}) {
15571
16105
  name,
15572
16106
  gherkin
15573
16107
  }
15574
- };
15575
- try {
15576
- const result = await context.invokeForgeContract("ingestBddFeature", payload);
15577
- if (!result.ok) {
16108
+ };
16109
+ try {
16110
+ const result = await context.invokeForgeContract("ingestBddFeature", payload);
16111
+ if (!result.ok) {
16112
+ if (useJson) {
16113
+ return {
16114
+ exitCode: ExitCode.RemoteError,
16115
+ stdout: toJsonLine({
16116
+ action: "ingest-feature",
16117
+ status: "failed",
16118
+ featureName: name,
16119
+ source: useStdin ? "stdin" : sourceFile,
16120
+ errorCode: result.error.code,
16121
+ errorMessage: result.error.message
16122
+ })
16123
+ };
16124
+ }
16125
+ return {
16126
+ exitCode: ExitCode.RemoteError,
16127
+ stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
16128
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
16129
+ };
16130
+ }
16131
+ if (useJson) {
16132
+ const details = summarizeSuccess(result);
16133
+ return {
16134
+ exitCode: ExitCode.Success,
16135
+ stdout: toJsonLine({
16136
+ action: "ingest-feature",
16137
+ status: "success",
16138
+ featureName: name,
16139
+ source: useStdin ? "stdin" : sourceFile,
16140
+ details
16141
+ })
16142
+ };
16143
+ }
16144
+ return {
16145
+ exitCode: ExitCode.Success,
16146
+ stdout: [
16147
+ "Feature ingestion: SUCCESS",
16148
+ `Feature: ${name}`,
16149
+ `Source: ${useStdin ? "stdin" : sourceFile}`,
16150
+ ...summarizeSuccess(result)
16151
+ ]
16152
+ };
16153
+ } catch (error) {
16154
+ if (useJson) {
16155
+ return {
16156
+ exitCode: ExitCode.TransportError,
16157
+ stdout: toJsonLine({
16158
+ action: "ingest-feature",
16159
+ status: "failed",
16160
+ featureName: name,
16161
+ source: useStdin ? "stdin" : sourceFile,
16162
+ errorMessage: normalizeError5(error)
16163
+ })
16164
+ };
16165
+ }
16166
+ return {
16167
+ exitCode: ExitCode.TransportError,
16168
+ stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
16169
+ stderr: [`ERROR: ${normalizeError5(error)}`]
16170
+ };
16171
+ }
16172
+ };
16173
+ }
16174
+
16175
+ // src/profiles.ts
16176
+ var CONFIG_FLAGS7 = /* @__PURE__ */ new Set([
16177
+ "--config",
16178
+ "--base-url",
16179
+ "--project-key",
16180
+ "--issue-key",
16181
+ "--auth-mode",
16182
+ "--jira-email",
16183
+ "--jira-api-token"
16184
+ ]);
16185
+ function parseArgs8(args) {
16186
+ const flags = {};
16187
+ const boolFlags = /* @__PURE__ */ new Set();
16188
+ const unknownFlags = [];
16189
+ const supportedValueFlags = /* @__PURE__ */ new Set(["--id", ...CONFIG_FLAGS7]);
16190
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--json", "--dry-run", "--confirm", "--detach"]);
16191
+ for (let index = 0; index < args.length; index += 1) {
16192
+ const token = args[index];
16193
+ if (!token.startsWith("--")) {
16194
+ unknownFlags.push(token);
16195
+ continue;
16196
+ }
16197
+ if (supportedBoolFlags.has(token)) {
16198
+ boolFlags.add(token);
16199
+ continue;
16200
+ }
16201
+ if (!supportedValueFlags.has(token)) {
16202
+ unknownFlags.push(token);
16203
+ continue;
16204
+ }
16205
+ const value = args[index + 1];
16206
+ if (!value || value.startsWith("--")) {
16207
+ unknownFlags.push(token);
16208
+ continue;
16209
+ }
16210
+ flags[token] = value;
16211
+ index += 1;
16212
+ }
16213
+ return { flags, boolFlags, unknownFlags };
16214
+ }
16215
+ function pickConfigArgs7(parsed) {
16216
+ const args = [];
16217
+ for (const [flag, value] of Object.entries(parsed.flags)) {
16218
+ if (CONFIG_FLAGS7.has(flag)) {
16219
+ args.push(flag, value);
16220
+ }
16221
+ }
16222
+ return args;
16223
+ }
16224
+ function normalizeError6(error) {
16225
+ if (error instanceof ForgeClientError) {
16226
+ return `${error.code}: ${error.message}`;
16227
+ }
16228
+ if (error instanceof Error) {
16229
+ return error.message;
16230
+ }
16231
+ return "Unknown profiles command error.";
16232
+ }
16233
+ function missingProjectResponse3() {
16234
+ return {
16235
+ exitCode: ExitCode.ValidationError,
16236
+ stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
16237
+ };
16238
+ }
16239
+ function confirmationRequiredResponse(command) {
16240
+ return {
16241
+ exitCode: ExitCode.ValidationError,
16242
+ stderr: [`ERROR: ${command} is destructive and requires --confirm. Use --dry-run first to preview the cleanup.`]
16243
+ };
16244
+ }
16245
+ function remoteErrorResponse(code, message, stdout = []) {
16246
+ return {
16247
+ exitCode: ExitCode.RemoteError,
16248
+ stdout,
16249
+ stderr: [`ERROR: ${code}: ${message}`]
16250
+ };
16251
+ }
16252
+ function toJsonProfile(profile) {
16253
+ return {
16254
+ id: profile.id,
16255
+ label: profile.label,
16256
+ provider: profile.provider,
16257
+ endpointSummary: profile.endpointSummary,
16258
+ enabled: profile.enabled,
16259
+ isProjectDefault: profile.isProjectDefault ?? false,
16260
+ hasSecret: profile.hasSecret,
16261
+ createdAt: profile.createdAt,
16262
+ updatedAt: profile.updatedAt,
16263
+ lastUsedAt: profile.lastUsedAt ?? null
16264
+ };
16265
+ }
16266
+ function toJsonBinding(automation) {
16267
+ return {
16268
+ id: automation.id,
16269
+ scenarioId: automation.scenarioId,
16270
+ label: automation.label,
16271
+ profileId: automation.profileId ?? null,
16272
+ profileLabel: automation.profileLabel ?? null,
16273
+ materializedFromProjectDefault: automation.materializedFromProjectDefault ?? false,
16274
+ enabled: automation.enabled
16275
+ };
16276
+ }
16277
+ function profileLines(profiles) {
16278
+ if (profiles.length === 0) {
16279
+ return ["Automation profiles: 0", "No automation profiles found for the selected project."];
16280
+ }
16281
+ return [
16282
+ `Automation profiles: ${profiles.length}`,
16283
+ ...profiles.map((profile) => {
16284
+ const flags = [profile.isProjectDefault ? "default" : "", profile.enabled ? "enabled" : "disabled"].filter(
16285
+ Boolean
16286
+ );
16287
+ return `- ${profile.id} [${flags.join(", ")}] ${profile.label} (${profile.provider}) ${profile.endpointSummary}`;
16288
+ })
16289
+ ];
16290
+ }
16291
+ function bindingLines(bindings) {
16292
+ return bindings.map(
16293
+ (binding) => `- ${binding.id} scenario=${binding.scenarioId} ${binding.label}` + (binding.materializedFromProjectDefault ? " [project-default materialized]" : "")
16294
+ );
16295
+ }
16296
+ function profileBindings(automations, profileId) {
16297
+ return automations.filter((automation) => automation.mode === "profile" && automation.profileId === profileId);
16298
+ }
16299
+ function allProfileBindings(automations) {
16300
+ return automations.filter((automation) => automation.mode === "profile");
16301
+ }
16302
+ function createProfilesHandler(deps = {}) {
16303
+ const cwd = deps.cwd ?? process.cwd();
16304
+ const env = deps.env ?? process.env;
16305
+ return async (request, context) => {
16306
+ const [subcommand, ...restArgs] = request.args;
16307
+ const parsed = parseArgs8(restArgs);
16308
+ const useJson = parsed.boolFlags.has("--json");
16309
+ const dryRun = parsed.boolFlags.has("--dry-run");
16310
+ const confirmed = parsed.boolFlags.has("--confirm");
16311
+ const detach = parsed.boolFlags.has("--detach");
16312
+ const profileId = parsed.flags["--id"]?.trim() ?? "";
16313
+ const config = resolveCliConfig(pickConfigArgs7(parsed), env, cwd);
16314
+ const projectKey = config.values.projectKey;
16315
+ const issueKey = config.values.issueKey;
16316
+ const forgeContext = {
16317
+ projectKey: projectKey || void 0,
16318
+ issueKey: issueKey || void 0
16319
+ };
16320
+ if (parsed.unknownFlags.length > 0) {
16321
+ return {
16322
+ exitCode: ExitCode.UsageError,
16323
+ stderr: [`ERROR: Unknown or invalid arguments: ${parsed.unknownFlags.join(", ")}`]
16324
+ };
16325
+ }
16326
+ if (!projectKey) {
16327
+ return missingProjectResponse3();
16328
+ }
16329
+ if (dryRun && confirmed) {
16330
+ return {
16331
+ exitCode: ExitCode.UsageError,
16332
+ stderr: ["ERROR: Use either --dry-run or --confirm, not both."]
16333
+ };
16334
+ }
16335
+ if (subcommand === "list") {
16336
+ if (dryRun || confirmed || detach || profileId) {
16337
+ return {
16338
+ exitCode: ExitCode.UsageError,
16339
+ stderr: ["ERROR: profiles list accepts only configuration flags and optional --json."]
16340
+ };
16341
+ }
16342
+ try {
16343
+ const result = await context.invokeForgeContract("listScenarioAutomationProfiles", {
16344
+ context: forgeContext
16345
+ });
16346
+ if (!result.ok) {
16347
+ return remoteErrorResponse(result.error.code, result.error.message);
16348
+ }
16349
+ if (useJson) {
16350
+ return {
16351
+ exitCode: ExitCode.Success,
16352
+ stdout: toJsonLine({
16353
+ action: "profiles-list",
16354
+ projectKey,
16355
+ count: result.data.length,
16356
+ items: result.data.map(toJsonProfile)
16357
+ })
16358
+ };
16359
+ }
16360
+ return {
16361
+ exitCode: ExitCode.Success,
16362
+ stdout: profileLines(result.data)
16363
+ };
16364
+ } catch (error) {
16365
+ return {
16366
+ exitCode: ExitCode.TransportError,
16367
+ stderr: [`ERROR: ${normalizeError6(error)}`]
16368
+ };
16369
+ }
16370
+ }
16371
+ if (subcommand === "detach") {
16372
+ if (!profileId) {
16373
+ return {
16374
+ exitCode: ExitCode.ValidationError,
16375
+ stderr: ["ERROR: Missing required --id <PROFILE_ID> for profiles detach."]
16376
+ };
16377
+ }
16378
+ if (detach) {
16379
+ return {
16380
+ exitCode: ExitCode.UsageError,
16381
+ stderr: [
16382
+ "ERROR: --detach is only valid with profiles delete; profiles detach already performs that operation."
16383
+ ]
16384
+ };
16385
+ }
16386
+ if (!dryRun && !confirmed) {
16387
+ return confirmationRequiredResponse("profiles detach");
16388
+ }
16389
+ try {
16390
+ const result = await context.invokeForgeContract("listScenarioAutomations", {
16391
+ context: forgeContext
16392
+ });
16393
+ if (!result.ok) {
16394
+ return remoteErrorResponse(result.error.code, result.error.message);
16395
+ }
16396
+ const bindings = profileBindings(result.data, profileId);
16397
+ if (dryRun) {
16398
+ if (useJson) {
16399
+ return {
16400
+ exitCode: ExitCode.Success,
16401
+ stdout: toJsonLine({
16402
+ action: "profiles-detach",
16403
+ dryRun: true,
16404
+ projectKey,
16405
+ profileId,
16406
+ bindingCount: bindings.length,
16407
+ bindings: bindings.map(toJsonBinding)
16408
+ })
16409
+ };
16410
+ }
16411
+ return {
16412
+ exitCode: ExitCode.Success,
16413
+ stdout: [
16414
+ `Dry run: profile ${profileId} has ${bindings.length} automation binding(s) to detach.`,
16415
+ ...bindingLines(bindings),
16416
+ "No changes made."
16417
+ ]
16418
+ };
16419
+ }
16420
+ const deletedIds = [];
16421
+ for (const binding of bindings) {
16422
+ const deleted = await context.invokeForgeContract("deleteScenarioAutomation", {
16423
+ context: forgeContext,
16424
+ automationId: binding.id
16425
+ });
16426
+ if (!deleted.ok) {
16427
+ return remoteErrorResponse(deleted.error.code, deleted.error.message, [
16428
+ `Detached ${deletedIds.length} of ${bindings.length} automation binding(s) before the failure.`
16429
+ ]);
16430
+ }
16431
+ deletedIds.push(binding.id);
16432
+ }
15578
16433
  if (useJson) {
15579
16434
  return {
15580
- exitCode: ExitCode.RemoteError,
16435
+ exitCode: ExitCode.Success,
15581
16436
  stdout: toJsonLine({
15582
- action: "ingest-feature",
15583
- status: "failed",
15584
- featureName: name,
15585
- source: useStdin ? "stdin" : sourceFile,
15586
- errorCode: result.error.code,
15587
- errorMessage: result.error.message
16437
+ action: "profiles-detach",
16438
+ dryRun: false,
16439
+ projectKey,
16440
+ profileId,
16441
+ detachedCount: deletedIds.length,
16442
+ detachedAutomationIds: deletedIds
15588
16443
  })
15589
16444
  };
15590
16445
  }
15591
16446
  return {
15592
- exitCode: ExitCode.RemoteError,
15593
- stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
15594
- stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
16447
+ exitCode: ExitCode.Success,
16448
+ stdout: [`Detached ${deletedIds.length} automation binding(s) from profile ${profileId}.`]
16449
+ };
16450
+ } catch (error) {
16451
+ return {
16452
+ exitCode: ExitCode.TransportError,
16453
+ stderr: [`ERROR: ${normalizeError6(error)}`]
15595
16454
  };
15596
16455
  }
15597
- if (useJson) {
15598
- const details = summarizeSuccess(result);
16456
+ }
16457
+ if (subcommand === "delete") {
16458
+ if (!profileId) {
15599
16459
  return {
15600
- exitCode: ExitCode.Success,
15601
- stdout: toJsonLine({
15602
- action: "ingest-feature",
15603
- status: "success",
15604
- featureName: name,
15605
- source: useStdin ? "stdin" : sourceFile,
15606
- details
15607
- })
16460
+ exitCode: ExitCode.ValidationError,
16461
+ stderr: ["ERROR: Missing required --id <PROFILE_ID> for profiles delete."]
15608
16462
  };
15609
16463
  }
15610
- return {
15611
- exitCode: ExitCode.Success,
15612
- stdout: [
15613
- "Feature ingestion: SUCCESS",
15614
- `Feature: ${name}`,
15615
- `Source: ${useStdin ? "stdin" : sourceFile}`,
15616
- ...summarizeSuccess(result)
15617
- ]
15618
- };
15619
- } catch (error) {
15620
- if (useJson) {
16464
+ if (!dryRun && !confirmed) {
16465
+ return confirmationRequiredResponse("profiles delete");
16466
+ }
16467
+ try {
16468
+ let bindings = [];
16469
+ if (detach || dryRun) {
16470
+ const automations = await context.invokeForgeContract("listScenarioAutomations", {
16471
+ context: forgeContext
16472
+ });
16473
+ if (!automations.ok) {
16474
+ return remoteErrorResponse(automations.error.code, automations.error.message);
16475
+ }
16476
+ bindings = profileBindings(automations.data, profileId);
16477
+ }
16478
+ if (dryRun) {
16479
+ if (useJson) {
16480
+ return {
16481
+ exitCode: ExitCode.Success,
16482
+ stdout: toJsonLine({
16483
+ action: "profiles-delete",
16484
+ dryRun: true,
16485
+ projectKey,
16486
+ profileId,
16487
+ detach,
16488
+ bindingCount: bindings.length,
16489
+ bindings: bindings.map(toJsonBinding)
16490
+ })
16491
+ };
16492
+ }
16493
+ return {
16494
+ exitCode: ExitCode.Success,
16495
+ stdout: [
16496
+ `Dry run: profile ${profileId} would be deleted.`,
16497
+ detach ? `The command would first detach ${bindings.length} automation binding(s).` : `The command would not detach ${bindings.length} automation binding(s); Forge will block deletion if the profile is in use.`,
16498
+ ...bindingLines(bindings),
16499
+ "No changes made."
16500
+ ]
16501
+ };
16502
+ }
16503
+ const detachedIds = [];
16504
+ if (detach) {
16505
+ for (const binding of bindings) {
16506
+ const deletedBinding = await context.invokeForgeContract("deleteScenarioAutomation", {
16507
+ context: forgeContext,
16508
+ automationId: binding.id
16509
+ });
16510
+ if (!deletedBinding.ok) {
16511
+ return remoteErrorResponse(deletedBinding.error.code, deletedBinding.error.message, [
16512
+ `Detached ${detachedIds.length} of ${bindings.length} automation binding(s); profile ${profileId} was not deleted.`
16513
+ ]);
16514
+ }
16515
+ detachedIds.push(binding.id);
16516
+ }
16517
+ }
16518
+ const deletedProfile = await context.invokeForgeContract("deleteScenarioAutomationProfile", {
16519
+ context: forgeContext,
16520
+ profileId
16521
+ });
16522
+ if (!deletedProfile.ok) {
16523
+ return remoteErrorResponse(deletedProfile.error.code, deletedProfile.error.message, [
16524
+ `Detached ${detachedIds.length} automation binding(s) before profile deletion was rejected.`
16525
+ ]);
16526
+ }
16527
+ if (useJson) {
16528
+ return {
16529
+ exitCode: ExitCode.Success,
16530
+ stdout: toJsonLine({
16531
+ action: "profiles-delete",
16532
+ dryRun: false,
16533
+ projectKey,
16534
+ profileId,
16535
+ detachedCount: detachedIds.length,
16536
+ detachedAutomationIds: detachedIds,
16537
+ deleted: true
16538
+ })
16539
+ };
16540
+ }
16541
+ return {
16542
+ exitCode: ExitCode.Success,
16543
+ stdout: [
16544
+ `Deleted automation profile ${profileId}.`,
16545
+ detach ? `Detached ${detachedIds.length} automation binding(s) first.` : "No automatic detach was requested."
16546
+ ]
16547
+ };
16548
+ } catch (error) {
15621
16549
  return {
15622
16550
  exitCode: ExitCode.TransportError,
15623
- stdout: toJsonLine({
15624
- action: "ingest-feature",
15625
- status: "failed",
15626
- featureName: name,
15627
- source: useStdin ? "stdin" : sourceFile,
15628
- errorMessage: normalizeError5(error)
16551
+ stderr: [`ERROR: ${normalizeError6(error)}`]
16552
+ };
16553
+ }
16554
+ }
16555
+ if (subcommand === "clear") {
16556
+ if (profileId || detach) {
16557
+ return {
16558
+ exitCode: ExitCode.UsageError,
16559
+ stderr: [
16560
+ "ERROR: profiles clear clears every profile and profile-based binding; do not pass --id or --detach."
16561
+ ]
16562
+ };
16563
+ }
16564
+ if (!dryRun && !confirmed) {
16565
+ return confirmationRequiredResponse("profiles clear");
16566
+ }
16567
+ try {
16568
+ const [profilesResult, automationsResult] = await Promise.all([
16569
+ context.invokeForgeContract("listScenarioAutomationProfiles", {
16570
+ context: forgeContext
16571
+ }),
16572
+ context.invokeForgeContract("listScenarioAutomations", {
16573
+ context: forgeContext
15629
16574
  })
16575
+ ]);
16576
+ if (!profilesResult.ok) {
16577
+ return remoteErrorResponse(profilesResult.error.code, profilesResult.error.message);
16578
+ }
16579
+ if (!automationsResult.ok) {
16580
+ return remoteErrorResponse(automationsResult.error.code, automationsResult.error.message);
16581
+ }
16582
+ const profiles = profilesResult.data;
16583
+ const bindings = allProfileBindings(automationsResult.data);
16584
+ if (dryRun) {
16585
+ if (useJson) {
16586
+ return {
16587
+ exitCode: ExitCode.Success,
16588
+ stdout: toJsonLine({
16589
+ action: "profiles-clear",
16590
+ dryRun: true,
16591
+ projectKey,
16592
+ profileCount: profiles.length,
16593
+ bindingCount: bindings.length,
16594
+ profiles: profiles.map(toJsonProfile),
16595
+ bindings: bindings.map(toJsonBinding)
16596
+ })
16597
+ };
16598
+ }
16599
+ return {
16600
+ exitCode: ExitCode.Success,
16601
+ stdout: [
16602
+ `Dry run: would detach ${bindings.length} profile-based automation binding(s) and delete ${profiles.length} automation profile(s).`,
16603
+ ...bindingLines(bindings),
16604
+ ...profiles.map((profile) => `- profile ${profile.id} ${profile.label}`),
16605
+ "Direct (non-profile) scenario automations are preserved.",
16606
+ "No changes made."
16607
+ ]
16608
+ };
16609
+ }
16610
+ const detachedIds = [];
16611
+ for (const binding of bindings) {
16612
+ const deletedBinding = await context.invokeForgeContract("deleteScenarioAutomation", {
16613
+ context: forgeContext,
16614
+ automationId: binding.id
16615
+ });
16616
+ if (!deletedBinding.ok) {
16617
+ return remoteErrorResponse(deletedBinding.error.code, deletedBinding.error.message, [
16618
+ `Detached ${detachedIds.length} of ${bindings.length} profile-based automation binding(s). No profiles were deleted after the failure.`
16619
+ ]);
16620
+ }
16621
+ detachedIds.push(binding.id);
16622
+ }
16623
+ const deletedProfileIds = [];
16624
+ for (const profile of profiles) {
16625
+ const deletedProfile = await context.invokeForgeContract("deleteScenarioAutomationProfile", {
16626
+ context: forgeContext,
16627
+ profileId: profile.id
16628
+ });
16629
+ if (!deletedProfile.ok) {
16630
+ return remoteErrorResponse(deletedProfile.error.code, deletedProfile.error.message, [
16631
+ `Detached ${detachedIds.length} profile-based automation binding(s).`,
16632
+ `Deleted ${deletedProfileIds.length} of ${profiles.length} automation profile(s) before the failure.`
16633
+ ]);
16634
+ }
16635
+ deletedProfileIds.push(profile.id);
16636
+ }
16637
+ if (useJson) {
16638
+ return {
16639
+ exitCode: ExitCode.Success,
16640
+ stdout: toJsonLine({
16641
+ action: "profiles-clear",
16642
+ dryRun: false,
16643
+ projectKey,
16644
+ detachedCount: detachedIds.length,
16645
+ detachedAutomationIds: detachedIds,
16646
+ deletedProfileCount: deletedProfileIds.length,
16647
+ deletedProfileIds
16648
+ })
16649
+ };
16650
+ }
16651
+ return {
16652
+ exitCode: ExitCode.Success,
16653
+ stdout: [
16654
+ `Detached ${detachedIds.length} profile-based automation binding(s).`,
16655
+ `Deleted ${deletedProfileIds.length} automation profile(s).`,
16656
+ "Direct (non-profile) scenario automations were preserved."
16657
+ ]
16658
+ };
16659
+ } catch (error) {
16660
+ return {
16661
+ exitCode: ExitCode.TransportError,
16662
+ stderr: [`ERROR: ${normalizeError6(error)}`]
15630
16663
  };
15631
16664
  }
15632
- return {
15633
- exitCode: ExitCode.TransportError,
15634
- stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
15635
- stderr: [`ERROR: ${normalizeError5(error)}`]
15636
- };
15637
16665
  }
16666
+ return {
16667
+ exitCode: ExitCode.UsageError,
16668
+ stderr: ["ERROR: Unsupported profiles subcommand. Use: list, detach, delete, or clear."]
16669
+ };
15638
16670
  };
15639
16671
  }
15640
16672
 
15641
16673
  // src/runUpload.ts
15642
- var import_node_fs12 = require("node:fs");
16674
+ var import_node_fs13 = require("node:fs");
15643
16675
  var import_node_path13 = __toESM(require("node:path"), 1);
15644
16676
  var STEP_RESULTS = [
15645
16677
  StepResult.Passed,
@@ -15647,7 +16679,7 @@ var STEP_RESULTS = [
15647
16679
  StepResult.Skipped,
15648
16680
  StepResult.Blocked
15649
16681
  ];
15650
- var CONFIG_FLAGS7 = /* @__PURE__ */ new Set([
16682
+ var CONFIG_FLAGS8 = /* @__PURE__ */ new Set([
15651
16683
  "--config",
15652
16684
  "--base-url",
15653
16685
  "--project-key",
@@ -15656,7 +16688,7 @@ var CONFIG_FLAGS7 = /* @__PURE__ */ new Set([
15656
16688
  "--jira-email",
15657
16689
  "--jira-api-token"
15658
16690
  ]);
15659
- function parseArgs8(args) {
16691
+ function parseArgs9(args) {
15660
16692
  const flags = {};
15661
16693
  const boolFlags = /* @__PURE__ */ new Set();
15662
16694
  const unknownFlags = [];
@@ -15667,7 +16699,7 @@ function parseArgs8(args) {
15667
16699
  "--feature-name",
15668
16700
  "--scenario-name",
15669
16701
  "--executed-at",
15670
- ...CONFIG_FLAGS7
16702
+ ...CONFIG_FLAGS8
15671
16703
  ]);
15672
16704
  const supportedBoolFlags = /* @__PURE__ */ new Set(["--stdin", "--json"]);
15673
16705
  for (let i = 0; i < args.length; i += 1) {
@@ -15693,16 +16725,16 @@ function parseArgs8(args) {
15693
16725
  }
15694
16726
  return { flags, boolFlags, unknownFlags };
15695
16727
  }
15696
- function pickConfigArgs7(parsed) {
16728
+ function pickConfigArgs8(parsed) {
15697
16729
  const configArgs = [];
15698
16730
  for (const [flag, value] of Object.entries(parsed.flags)) {
15699
- if (CONFIG_FLAGS7.has(flag)) {
16731
+ if (CONFIG_FLAGS8.has(flag)) {
15700
16732
  configArgs.push(flag, value);
15701
16733
  }
15702
16734
  }
15703
16735
  return configArgs;
15704
16736
  }
15705
- function normalizeError6(error) {
16737
+ function normalizeError7(error) {
15706
16738
  if (error instanceof ForgeClientError) {
15707
16739
  return `${error.code}: ${error.message}`;
15708
16740
  }
@@ -15781,14 +16813,14 @@ function summarizeRunResult(result) {
15781
16813
  return lines;
15782
16814
  }
15783
16815
  function createRunUploadHandler(deps = {}) {
15784
- const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs12.readFileSync)(filePath, "utf8"));
15785
- const readStdin = deps.readStdin ?? (() => (0, import_node_fs12.readFileSync)(0, "utf8"));
16816
+ const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs13.readFileSync)(filePath, "utf8"));
16817
+ const readStdin = deps.readStdin ?? (() => (0, import_node_fs13.readFileSync)(0, "utf8"));
15786
16818
  const cwd = deps.cwd ?? process.cwd();
15787
16819
  const env = deps.env ?? process.env;
15788
16820
  return async (request, context) => {
15789
16821
  const [, ...subArgs] = request.args;
15790
- const parsed = parseArgs8(subArgs);
15791
- const configArgs = pickConfigArgs7(parsed);
16822
+ const parsed = parseArgs9(subArgs);
16823
+ const configArgs = pickConfigArgs8(parsed);
15792
16824
  const config = resolveCliConfig(configArgs, env, cwd);
15793
16825
  const projectKey = config.values.projectKey;
15794
16826
  const issueKey = config.values.issueKey;
@@ -15821,7 +16853,7 @@ function createRunUploadHandler(deps = {}) {
15821
16853
  } catch (error) {
15822
16854
  return {
15823
16855
  exitCode: ExitCode.InternalError,
15824
- stderr: [`ERROR: Failed to read run payload source: ${normalizeError6(error)}`]
16856
+ stderr: [`ERROR: Failed to read run payload source: ${normalizeError7(error)}`]
15825
16857
  };
15826
16858
  }
15827
16859
  const payloadFromSource = parseRunPayload(raw);
@@ -15851,7 +16883,7 @@ function createRunUploadHandler(deps = {}) {
15851
16883
  } catch (error) {
15852
16884
  return {
15853
16885
  exitCode: ExitCode.TransportError,
15854
- stderr: [`ERROR: ${normalizeError6(error)}`]
16886
+ stderr: [`ERROR: ${normalizeError7(error)}`]
15855
16887
  };
15856
16888
  }
15857
16889
  }
@@ -15931,7 +16963,7 @@ function createRunUploadHandler(deps = {}) {
15931
16963
  featureName: runInput.featureName,
15932
16964
  scenarioName: runInput.scenarioName,
15933
16965
  executedAt: runInput.executedAt,
15934
- errorMessage: normalizeError6(error)
16966
+ errorMessage: normalizeError7(error)
15935
16967
  })
15936
16968
  };
15937
16969
  }
@@ -15943,14 +16975,14 @@ function createRunUploadHandler(deps = {}) {
15943
16975
  `Scenario: ${runInput.scenarioName}`,
15944
16976
  `ExecutedAt: ${runInput.executedAt}`
15945
16977
  ],
15946
- stderr: [`ERROR: ${normalizeError6(error)}`]
16978
+ stderr: [`ERROR: ${normalizeError7(error)}`]
15947
16979
  };
15948
16980
  }
15949
16981
  };
15950
16982
  }
15951
16983
 
15952
16984
  // src/runs.ts
15953
- var CONFIG_FLAGS8 = /* @__PURE__ */ new Set([
16985
+ var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
15954
16986
  "--config",
15955
16987
  "--base-url",
15956
16988
  "--project-key",
@@ -15959,11 +16991,11 @@ var CONFIG_FLAGS8 = /* @__PURE__ */ new Set([
15959
16991
  "--jira-email",
15960
16992
  "--jira-api-token"
15961
16993
  ]);
15962
- function parseArgs9(args) {
16994
+ function parseArgs10(args) {
15963
16995
  const flags = {};
15964
16996
  const boolFlags = /* @__PURE__ */ new Set();
15965
16997
  const unknownFlags = [];
15966
- const supportedValueFlags = /* @__PURE__ */ new Set(["--id", ...CONFIG_FLAGS8]);
16998
+ const supportedValueFlags = /* @__PURE__ */ new Set(["--id", ...CONFIG_FLAGS9]);
15967
16999
  const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
15968
17000
  for (let index = 0; index < args.length; index += 1) {
15969
17001
  const token = args[index];
@@ -15988,16 +17020,16 @@ function parseArgs9(args) {
15988
17020
  }
15989
17021
  return { flags, boolFlags, unknownFlags };
15990
17022
  }
15991
- function pickConfigArgs8(parsed) {
17023
+ function pickConfigArgs9(parsed) {
15992
17024
  const args = [];
15993
17025
  for (const [flag, value] of Object.entries(parsed.flags)) {
15994
- if (CONFIG_FLAGS8.has(flag)) {
17026
+ if (CONFIG_FLAGS9.has(flag)) {
15995
17027
  args.push(flag, value);
15996
17028
  }
15997
17029
  }
15998
17030
  return args;
15999
17031
  }
16000
- function normalizeError7(error) {
17032
+ function normalizeError8(error) {
16001
17033
  if (error instanceof ForgeClientError) {
16002
17034
  return `${error.code}: ${error.message}`;
16003
17035
  }
@@ -16050,7 +17082,7 @@ function runToLines(run) {
16050
17082
  }
16051
17083
  return lines;
16052
17084
  }
16053
- function missingProjectResponse3() {
17085
+ function missingProjectResponse4() {
16054
17086
  return {
16055
17087
  exitCode: ExitCode.ValidationError,
16056
17088
  stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
@@ -16061,9 +17093,9 @@ function createRunsHandler(deps = {}) {
16061
17093
  const env = deps.env ?? process.env;
16062
17094
  return async (request, context) => {
16063
17095
  const [subcommand, ...restArgs] = request.args;
16064
- const parsed = parseArgs9(restArgs);
17096
+ const parsed = parseArgs10(restArgs);
16065
17097
  const useJson = parsed.boolFlags.has("--json");
16066
- const config = resolveCliConfig(pickConfigArgs8(parsed), env, cwd);
17098
+ const config = resolveCliConfig(pickConfigArgs9(parsed), env, cwd);
16067
17099
  const projectKey = config.values.projectKey;
16068
17100
  const issueKey = config.values.issueKey;
16069
17101
  if (parsed.unknownFlags.length > 0) {
@@ -16073,7 +17105,7 @@ function createRunsHandler(deps = {}) {
16073
17105
  };
16074
17106
  }
16075
17107
  if (!projectKey) {
16076
- return missingProjectResponse3();
17108
+ return missingProjectResponse4();
16077
17109
  }
16078
17110
  if (subcommand === "show") {
16079
17111
  const runId = parsed.flags["--id"]?.trim() ?? "";
@@ -16115,7 +17147,7 @@ function createRunsHandler(deps = {}) {
16115
17147
  } catch (error) {
16116
17148
  return {
16117
17149
  exitCode: ExitCode.TransportError,
16118
- stderr: [`ERROR: ${normalizeError7(error)}`]
17150
+ stderr: [`ERROR: ${normalizeError8(error)}`]
16119
17151
  };
16120
17152
  }
16121
17153
  }
@@ -16127,7 +17159,7 @@ function createRunsHandler(deps = {}) {
16127
17159
  }
16128
17160
 
16129
17161
  // src/setup.ts
16130
- var import_node_fs14 = require("node:fs");
17162
+ var import_node_fs15 = require("node:fs");
16131
17163
  var import_node_path15 = __toESM(require("node:path"), 1);
16132
17164
 
16133
17165
  // src/githubSetupProvider.ts
@@ -16135,7 +17167,7 @@ var import_node_child_process4 = require("node:child_process");
16135
17167
 
16136
17168
  // src/setupPlan.ts
16137
17169
  var import_node_crypto = require("node:crypto");
16138
- var import_node_fs13 = require("node:fs");
17170
+ var import_node_fs14 = require("node:fs");
16139
17171
  var import_node_path14 = __toESM(require("node:path"), 1);
16140
17172
  var SETUP_PLAN_SCHEMA_VERSION = "automatify.testops.setup/v1";
16141
17173
  var SETUP_PLAN_KIND = "AutomatifyTestOpsSetupPlan";
@@ -16231,7 +17263,7 @@ function buildGitHubSetupPlan(input) {
16231
17263
  if (endpointSecretName === tokenSecretName) {
16232
17264
  throw new Error("callback endpoint and auth token secret names must be different.");
16233
17265
  }
16234
- const workflowContent = (0, import_node_fs13.readFileSync)(input.workflowSourcePath, "utf8");
17266
+ const workflowContent = (0, import_node_fs14.readFileSync)(input.workflowSourcePath, "utf8");
16235
17267
  if (!workflowContent.trim()) {
16236
17268
  throw new Error("workflow source file is empty.");
16237
17269
  }
@@ -17095,7 +18127,7 @@ function defaultReadStdin() {
17095
18127
  process.stdin.on("error", reject);
17096
18128
  });
17097
18129
  }
17098
- function parseArgs10(args, valueFlags, boolFlags, allowApplyCollections = false) {
18130
+ function parseArgs11(args, valueFlags, boolFlags, allowApplyCollections = false) {
17099
18131
  const flags = {};
17100
18132
  const enabled = /* @__PURE__ */ new Set();
17101
18133
  const approvals = [];
@@ -17177,7 +18209,7 @@ function loadPlan(planPath, cwd) {
17177
18209
  const absolute = import_node_path15.default.resolve(cwd, planPath);
17178
18210
  let parsed;
17179
18211
  try {
17180
- parsed = JSON.parse((0, import_node_fs14.readFileSync)(absolute, "utf8"));
18212
+ parsed = JSON.parse((0, import_node_fs15.readFileSync)(absolute, "utf8"));
17181
18213
  } catch (error) {
17182
18214
  throw new SetupCommandError(
17183
18215
  "PLAN_READ_ERROR",
@@ -17416,7 +18448,7 @@ function resolveWorkflowTarget(plan, repoRoot) {
17416
18448
  function applyWorkflowFile(plan, repoRoot, dryRun) {
17417
18449
  const action = plan.actions.find((item) => item.scope === "workflow-file");
17418
18450
  const target = resolveWorkflowTarget(plan, repoRoot);
17419
- const sourceContent = (0, import_node_fs14.readFileSync)(plan.github.workflowFile.sourcePath, "utf8");
18451
+ const sourceContent = (0, import_node_fs15.readFileSync)(plan.github.workflowFile.sourcePath, "utf8");
17420
18452
  if (hashSetupContent(sourceContent) !== plan.github.workflowFile.sha256) {
17421
18453
  throw new SetupCommandError(
17422
18454
  "WORKFLOW_SOURCE_CHANGED",
@@ -17424,8 +18456,8 @@ function applyWorkflowFile(plan, repoRoot, dryRun) {
17424
18456
  ExitCode.ValidationError
17425
18457
  );
17426
18458
  }
17427
- const existed = (0, import_node_fs14.existsSync)(target);
17428
- const previousContent = existed ? (0, import_node_fs14.readFileSync)(target, "utf8") : void 0;
18459
+ const existed = (0, import_node_fs15.existsSync)(target);
18460
+ const previousContent = existed ? (0, import_node_fs15.readFileSync)(target, "utf8") : void 0;
17429
18461
  const matches = previousContent !== void 0 && hashSetupContent(previousContent) === plan.github.workflowFile.sha256;
17430
18462
  if (matches) {
17431
18463
  return {
@@ -17437,8 +18469,8 @@ function applyWorkflowFile(plan, repoRoot, dryRun) {
17437
18469
  };
17438
18470
  }
17439
18471
  if (!dryRun) {
17440
- (0, import_node_fs14.mkdirSync)(import_node_path15.default.dirname(target), { recursive: true });
17441
- (0, import_node_fs14.writeFileSync)(target, sourceContent, { encoding: "utf8", mode: 420 });
18472
+ (0, import_node_fs15.mkdirSync)(import_node_path15.default.dirname(target), { recursive: true });
18473
+ (0, import_node_fs15.writeFileSync)(target, sourceContent, { encoding: "utf8", mode: 420 });
17442
18474
  }
17443
18475
  return {
17444
18476
  id: action.id,
@@ -17608,14 +18640,14 @@ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
17608
18640
  const checks = [];
17609
18641
  let forgeTransportFailure = false;
17610
18642
  const target = resolveWorkflowTarget(plan, repoRoot);
17611
- if (!(0, import_node_fs14.existsSync)(target)) {
18643
+ if (!(0, import_node_fs15.existsSync)(target)) {
17612
18644
  checks.push({
17613
18645
  id: "workflow-local",
17614
18646
  status: "fail",
17615
18647
  message: `Local workflow file ${plan.github.workflowFile.path} does not exist.`
17616
18648
  });
17617
18649
  } else {
17618
- const localHash = hashSetupContent((0, import_node_fs14.readFileSync)(target, "utf8"));
18650
+ const localHash = hashSetupContent((0, import_node_fs15.readFileSync)(target, "utf8"));
17619
18651
  checks.push({
17620
18652
  id: "workflow-local",
17621
18653
  status: localHash === plan.github.workflowFile.sha256 ? "pass" : "fail",
@@ -17705,7 +18737,7 @@ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
17705
18737
  };
17706
18738
  }
17707
18739
  function planCommand(args, deps) {
17708
- const parsed = parseArgs10(args, PLAN_VALUE_FLAGS, /* @__PURE__ */ new Set(["--set-default", "--disabled"]));
18740
+ const parsed = parseArgs11(args, PLAN_VALUE_FLAGS, /* @__PURE__ */ new Set(["--set-default", "--disabled"]));
17709
18741
  if (parsed.errors.length > 0) {
17710
18742
  return jsonResponse(
17711
18743
  errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors),
@@ -17764,7 +18796,7 @@ function planCommand(args, deps) {
17764
18796
  }
17765
18797
  }
17766
18798
  async function applyCommand(args, context, deps) {
17767
- const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, APPLY_BOOL_FLAGS, true);
18799
+ const parsed = parseArgs11(args, APPLY_VALUE_FLAGS, APPLY_BOOL_FLAGS, true);
17768
18800
  if (parsed.errors.length > 0) {
17769
18801
  return jsonResponse(
17770
18802
  errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors),
@@ -18007,7 +19039,7 @@ async function applyAzureCommand(plan, parsed, context, deps, secrets) {
18007
19039
  });
18008
19040
  }
18009
19041
  async function doctorCommand(args, context, deps) {
18010
- const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, /* @__PURE__ */ new Set(["--secrets-stdin"]), true);
19042
+ const parsed = parseArgs11(args, APPLY_VALUE_FLAGS, /* @__PURE__ */ new Set(["--secrets-stdin"]), true);
18011
19043
  if (parsed.approvals.length > 0) {
18012
19044
  return jsonResponse(errorPayload("USAGE_ERROR", "setup doctor does not accept --approve."), ExitCode.UsageError);
18013
19045
  }
@@ -18120,7 +19152,7 @@ function createSetupHandler(overrides = {}) {
18120
19152
  }
18121
19153
 
18122
19154
  // src/suites.ts
18123
- var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
19155
+ var CONFIG_FLAGS10 = /* @__PURE__ */ new Set([
18124
19156
  "--config",
18125
19157
  "--base-url",
18126
19158
  "--project-key",
@@ -18129,11 +19161,11 @@ var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
18129
19161
  "--jira-email",
18130
19162
  "--jira-api-token"
18131
19163
  ]);
18132
- function parseArgs11(args) {
19164
+ function parseArgs12(args) {
18133
19165
  const flags = {};
18134
19166
  const boolFlags = /* @__PURE__ */ new Set();
18135
19167
  const unknownFlags = [];
18136
- const supportedValueFlags = /* @__PURE__ */ new Set(["--key", ...CONFIG_FLAGS9]);
19168
+ const supportedValueFlags = /* @__PURE__ */ new Set(["--key", ...CONFIG_FLAGS10]);
18137
19169
  const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
18138
19170
  for (let index = 0; index < args.length; index += 1) {
18139
19171
  const token = args[index];
@@ -18158,16 +19190,16 @@ function parseArgs11(args) {
18158
19190
  }
18159
19191
  return { flags, boolFlags, unknownFlags };
18160
19192
  }
18161
- function pickConfigArgs9(parsed) {
19193
+ function pickConfigArgs10(parsed) {
18162
19194
  const args = [];
18163
19195
  for (const [flag, value] of Object.entries(parsed.flags)) {
18164
- if (CONFIG_FLAGS9.has(flag)) {
19196
+ if (CONFIG_FLAGS10.has(flag)) {
18165
19197
  args.push(flag, value);
18166
19198
  }
18167
19199
  }
18168
19200
  return args;
18169
19201
  }
18170
- function normalizeError8(error) {
19202
+ function normalizeError9(error) {
18171
19203
  if (error instanceof ForgeClientError) {
18172
19204
  return `${error.code}: ${error.message}`;
18173
19205
  }
@@ -18229,7 +19261,7 @@ function suiteCasesToLines(suite, items) {
18229
19261
  ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
18230
19262
  ];
18231
19263
  }
18232
- function missingProjectResponse4() {
19264
+ function missingProjectResponse5() {
18233
19265
  return {
18234
19266
  exitCode: ExitCode.ValidationError,
18235
19267
  stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
@@ -18246,9 +19278,9 @@ function createSuitesHandler(deps = {}) {
18246
19278
  const env = deps.env ?? process.env;
18247
19279
  return async (request, context) => {
18248
19280
  const [subcommand, ...restArgs] = request.args;
18249
- const parsed = parseArgs11(restArgs);
19281
+ const parsed = parseArgs12(restArgs);
18250
19282
  const useJson = parsed.boolFlags.has("--json");
18251
- const config = resolveCliConfig(pickConfigArgs9(parsed), env, cwd);
19283
+ const config = resolveCliConfig(pickConfigArgs10(parsed), env, cwd);
18252
19284
  const projectKey = config.values.projectKey;
18253
19285
  const issueKey = config.values.issueKey;
18254
19286
  if (parsed.unknownFlags.length > 0) {
@@ -18258,7 +19290,7 @@ function createSuitesHandler(deps = {}) {
18258
19290
  };
18259
19291
  }
18260
19292
  if (!projectKey) {
18261
- return missingProjectResponse4();
19293
+ return missingProjectResponse5();
18262
19294
  }
18263
19295
  if (subcommand === "list") {
18264
19296
  try {
@@ -18293,7 +19325,7 @@ function createSuitesHandler(deps = {}) {
18293
19325
  } catch (error) {
18294
19326
  return {
18295
19327
  exitCode: ExitCode.TransportError,
18296
- stderr: [`ERROR: ${normalizeError8(error)}`]
19328
+ stderr: [`ERROR: ${normalizeError9(error)}`]
18297
19329
  };
18298
19330
  }
18299
19331
  }
@@ -18334,7 +19366,7 @@ function createSuitesHandler(deps = {}) {
18334
19366
  } catch (error) {
18335
19367
  return {
18336
19368
  exitCode: ExitCode.TransportError,
18337
- stderr: [`ERROR: ${normalizeError8(error)}`]
19369
+ stderr: [`ERROR: ${normalizeError9(error)}`]
18338
19370
  };
18339
19371
  }
18340
19372
  }
@@ -18398,7 +19430,7 @@ function createSuitesHandler(deps = {}) {
18398
19430
  } catch (error) {
18399
19431
  return {
18400
19432
  exitCode: ExitCode.TransportError,
18401
- stderr: [`ERROR: ${normalizeError8(error)}`]
19433
+ stderr: [`ERROR: ${normalizeError9(error)}`]
18402
19434
  };
18403
19435
  }
18404
19436
  }
@@ -18410,7 +19442,7 @@ function createSuitesHandler(deps = {}) {
18410
19442
  }
18411
19443
 
18412
19444
  // src/sync.ts
18413
- var CONFIG_FLAGS10 = /* @__PURE__ */ new Set([
19445
+ var CONFIG_FLAGS11 = /* @__PURE__ */ new Set([
18414
19446
  "--config",
18415
19447
  "--base-url",
18416
19448
  "--project-key",
@@ -18420,11 +19452,11 @@ var CONFIG_FLAGS10 = /* @__PURE__ */ new Set([
18420
19452
  "--jira-api-token"
18421
19453
  ]);
18422
19454
  var RECONCILE_CONFIRM_TOKEN = "RECONCILE";
18423
- function parseArgs12(args) {
19455
+ function parseArgs13(args) {
18424
19456
  const flags = {};
18425
19457
  const boolFlags = /* @__PURE__ */ new Set();
18426
19458
  const unknownFlags = [];
18427
- const supportedValueFlags = /* @__PURE__ */ new Set(["--confirm", ...CONFIG_FLAGS10]);
19459
+ const supportedValueFlags = /* @__PURE__ */ new Set(["--confirm", ...CONFIG_FLAGS11]);
18428
19460
  const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
18429
19461
  for (let i = 0; i < args.length; i += 1) {
18430
19462
  const token = args[i];
@@ -18449,16 +19481,16 @@ function parseArgs12(args) {
18449
19481
  }
18450
19482
  return { flags, boolFlags, unknownFlags };
18451
19483
  }
18452
- function pickConfigArgs10(parsed) {
19484
+ function pickConfigArgs11(parsed) {
18453
19485
  const args = [];
18454
19486
  for (const [flag, value] of Object.entries(parsed.flags)) {
18455
- if (CONFIG_FLAGS10.has(flag)) {
19487
+ if (CONFIG_FLAGS11.has(flag)) {
18456
19488
  args.push(flag, value);
18457
19489
  }
18458
19490
  }
18459
19491
  return args;
18460
19492
  }
18461
- function normalizeError9(error) {
19493
+ function normalizeError10(error) {
18462
19494
  if (error instanceof ForgeClientError) {
18463
19495
  return `${error.code}: ${error.message}`;
18464
19496
  }
@@ -18509,9 +19541,9 @@ function createSyncHandler(deps = {}) {
18509
19541
  const env = deps.env ?? process.env;
18510
19542
  return async (request, context) => {
18511
19543
  const [subcommand, ...restArgs] = request.args;
18512
- const parsed = parseArgs12(restArgs);
19544
+ const parsed = parseArgs13(restArgs);
18513
19545
  const useJson = parsed.boolFlags.has("--json");
18514
- const config = resolveCliConfig(pickConfigArgs10(parsed), env, cwd);
19546
+ const config = resolveCliConfig(pickConfigArgs11(parsed), env, cwd);
18515
19547
  const projectKey = config.values.projectKey;
18516
19548
  const issueKey = config.values.issueKey;
18517
19549
  if (parsed.unknownFlags.length > 0) {
@@ -18561,7 +19593,7 @@ function createSyncHandler(deps = {}) {
18561
19593
  } catch (error) {
18562
19594
  return {
18563
19595
  exitCode: ExitCode.TransportError,
18564
- stderr: [`ERROR: ${normalizeError9(error)}`]
19596
+ stderr: [`ERROR: ${normalizeError10(error)}`]
18565
19597
  };
18566
19598
  }
18567
19599
  }
@@ -18607,7 +19639,7 @@ function createSyncHandler(deps = {}) {
18607
19639
  } catch (error) {
18608
19640
  return {
18609
19641
  exitCode: ExitCode.TransportError,
18610
- stderr: [`ERROR: ${normalizeError9(error)}`]
19642
+ stderr: [`ERROR: ${normalizeError10(error)}`]
18611
19643
  };
18612
19644
  }
18613
19645
  }
@@ -18626,6 +19658,12 @@ var COMMAND_REGISTRY = [
18626
19658
  subcommands: ["upload", "download", "open"],
18627
19659
  handler: createAllureHandler()
18628
19660
  },
19661
+ {
19662
+ name: "auth",
19663
+ description: "Secret-safe Jira authentication login, status, and logout commands",
19664
+ subcommands: ["login", "status", "logout"],
19665
+ handler: createAuthHandler()
19666
+ },
18629
19667
  {
18630
19668
  name: "auto",
18631
19669
  description: "Smart auto-ingestion command contract (MVP staged)",
@@ -18648,7 +19686,7 @@ var COMMAND_REGISTRY = [
18648
19686
  {
18649
19687
  name: "config",
18650
19688
  description: "Configuration and auth bootstrap commands",
18651
- subcommands: ["show", "validate", "set"],
19689
+ subcommands: ["show", "validate", "set", "unset", "migrate-secrets"],
18652
19690
  handler: createConfigHandler()
18653
19691
  },
18654
19692
  {
@@ -18657,6 +19695,12 @@ var COMMAND_REGISTRY = [
18657
19695
  subcommands: ["feature"],
18658
19696
  handler: createIngestFeatureHandler()
18659
19697
  },
19698
+ {
19699
+ name: "profiles",
19700
+ description: "Automation profile inspection, detach, delete, and demo cleanup commands",
19701
+ subcommands: ["list", "detach", "delete", "clear"],
19702
+ handler: createProfilesHandler()
19703
+ },
18660
19704
  {
18661
19705
  name: "run",
18662
19706
  description: "Execution result upload commands",