@automatify-au/cli 0.1.13 → 0.1.15

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 +20 -6
  2. package/dist/automatify.cjs +1285 -358
  3. package/package.json +4 -4
@@ -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,10 +10983,33 @@ 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");
10820
- (0, import_node_fs.chmodSync)(configPath, 384);
11010
+ if (process.platform !== "win32") {
11011
+ (0, import_node_fs.chmodSync)(configPath, 384);
11012
+ }
10821
11013
  }
10822
11014
  function normalizeBaseUrl(value) {
10823
11015
  if (value.endsWith("/")) {
@@ -10831,28 +11023,46 @@ function readAuthMode(input) {
10831
11023
  function defaultKeychainAccount(configPath) {
10832
11024
  return import_node_path.default.resolve(configPath);
10833
11025
  }
10834
- function readKeychainSecret(account) {
10835
- if (!account || process.platform !== "darwin") {
10836
- 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" };
10837
11032
  }
10838
- try {
10839
- return (0, import_node_child_process.execFileSync)("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"], {
10840
- encoding: "utf8",
10841
- stdio: ["ignore", "pipe", "ignore"]
10842
- }).trim();
10843
- } catch {
10844
- return "";
11033
+ if (envValue) {
11034
+ return { value: envValue, source: "env" };
10845
11035
  }
11036
+ if (fileValue) {
11037
+ return { value: fileValue, source: "file" };
11038
+ }
11039
+ if (fallback) {
11040
+ return { value: fallback, source: "default" };
11041
+ }
11042
+ return { value: "", source: "default" };
10846
11043
  }
10847
- function setKeychainSecret(account, value) {
10848
- if (process.platform !== "darwin") {
10849
- throw new Error("Keychain-backed secrets are only supported on macOS.");
11044
+ function secureAccountForKey(key, file, configPath) {
11045
+ if (key === "forgeAuthToken") {
11046
+ return toStringValue(file.forgeAuthTokenKeychainAccount) || defaultKeychainAccount(configPath);
10850
11047
  }
10851
- (0, import_node_child_process.execFileSync)("security", ["add-generic-password", "-U", "-s", KEYCHAIN_SERVICE, "-a", account, "-w", value], {
10852
- stdio: ["ignore", "ignore", "pipe"]
10853
- });
11048
+ return toStringValue(file.jiraApiTokenKeychainAccount) || defaultJiraApiTokenKeychainAccount(configPath);
10854
11049
  }
10855
- function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "") {
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
+ };
11056
+ }
11057
+ return {
11058
+ account: toStringValue(file.jiraApiTokenKeychainAccount) || void 0,
11059
+ protectedValue: toStringValue(file.jiraApiTokenProtected) || void 0
11060
+ };
11061
+ }
11062
+ function secretEnvironmentName(key) {
11063
+ return key === "jiraApiToken" ? "JIRA_API_TOKEN" : "TESTOPS_FORGE_AUTH_TOKEN";
11064
+ }
11065
+ function resolveSecretValue(key, flagValue, envValue, fileValue, file) {
10856
11066
  if (flagValue) {
10857
11067
  return { value: flagValue, source: "flag" };
10858
11068
  }
@@ -10862,11 +11072,33 @@ function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "")
10862
11072
  if (fileValue) {
10863
11073
  return { value: fileValue, source: "file" };
10864
11074
  }
10865
- if (fallback) {
10866
- 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}` };
10867
11081
  }
10868
11082
  return { value: "", source: "default" };
10869
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
+ }
10870
11102
  function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
10871
11103
  const { flags, unknownFlags } = parseFlags(args);
10872
11104
  const configPathFlag = flags["--config"] ? import_node_path.default.resolve(cwd, flags["--config"]) : "";
@@ -10905,11 +11137,12 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
10905
11137
  toStringValue(env.JIRA_EMAIL),
10906
11138
  toStringValue(file.jiraEmail ?? file.JIRA_EMAIL)
10907
11139
  );
10908
- const jiraApiTokenResolved = valueFromPrecedence(
11140
+ const jiraApiTokenResolved = resolveSecretValue(
10909
11141
  "jiraApiToken",
10910
11142
  toStringValue(flags["--jira-api-token"]),
10911
11143
  toStringValue(env.JIRA_API_TOKEN),
10912
- toStringValue(file.jiraApiToken ?? file.JIRA_API_TOKEN)
11144
+ firstStringValue(file.jiraApiToken, file.JIRA_API_TOKEN),
11145
+ file
10913
11146
  );
10914
11147
  const forgeEndpointResolved = valueFromPrecedence(
10915
11148
  "forgeEndpoint",
@@ -10923,10 +11156,13 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
10923
11156
  "",
10924
11157
  toStringValue(file.forgeAuthTokenKeychainAccount)
10925
11158
  );
10926
- const forgeAuthTokenFromEnv = toStringValue(env.TESTOPS_FORGE_AUTH_TOKEN);
10927
- const forgeAuthTokenFromFile = toStringValue(file.forgeAuthToken ?? file.TESTOPS_FORGE_AUTH_TOKEN);
10928
- const forgeAuthTokenFromKeychain = forgeAuthTokenFromEnv || forgeAuthTokenFromFile || !forgeAuthTokenKeychainAccountResolved.value ? "" : readKeychainSecret(forgeAuthTokenKeychainAccountResolved.value);
10929
- const forgeAuthTokenResolved = forgeAuthTokenFromEnv ? { value: forgeAuthTokenFromEnv, source: "env" } : forgeAuthTokenFromFile ? { value: forgeAuthTokenFromFile, source: "file" } : forgeAuthTokenFromKeychain ? { value: forgeAuthTokenFromKeychain, source: "keychain" } : { 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
+ );
10930
11166
  const config = {
10931
11167
  baseUrl: normalizeBaseUrl(baseUrlResolved.value),
10932
11168
  projectKey: projectKeyResolved.value,
@@ -10952,13 +11188,16 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
10952
11188
  configPath: configPathSource
10953
11189
  };
10954
11190
  const warnings = [];
11191
+ const errors = [jiraApiTokenResolved.error, forgeAuthTokenResolved.error].filter(
11192
+ (value) => Boolean(value)
11193
+ );
10955
11194
  if (unknownFlags.length > 0) {
10956
11195
  warnings.push(`Ignored unknown config flags: ${unknownFlags.join(", ")}`);
10957
11196
  }
10958
11197
  if (!(0, import_node_fs.existsSync)(configPath) && sources.configPath !== "default") {
10959
11198
  warnings.push(`Config file not found at ${configPath}; using env/flags/defaults.`);
10960
11199
  }
10961
- return { values: config, sources, warnings };
11200
+ return { values: config, sources, warnings, errors };
10962
11201
  }
10963
11202
  function maskSecret(value) {
10964
11203
  if (!value) {
@@ -10971,7 +11210,7 @@ function maskSecret(value) {
10971
11210
  }
10972
11211
  function validateResolvedConfig(resolution) {
10973
11212
  const { values } = resolution;
10974
- const errors = [];
11213
+ const errors = [...resolution.errors];
10975
11214
  const warnings = [...resolution.warnings];
10976
11215
  if (!values.baseUrl) {
10977
11216
  errors.push("Missing required setting: JIRA_BASE_URL (flag/env/file).");
@@ -10986,10 +11225,8 @@ function validateResolvedConfig(resolution) {
10986
11225
  if (!values.jiraApiToken) {
10987
11226
  errors.push("Auth mode api-token requires JIRA_API_TOKEN.");
10988
11227
  }
10989
- } else {
10990
- if (values.jiraApiToken || values.jiraEmail) {
10991
- warnings.push("JIRA_EMAIL/JIRA_API_TOKEN were provided but auth mode is 'none'.");
10992
- }
11228
+ } else if (values.jiraApiToken || values.jiraEmail) {
11229
+ warnings.push("JIRA_EMAIL/JIRA_API_TOKEN were provided but auth mode is 'none'.");
10993
11230
  }
10994
11231
  return { ok: errors.length === 0, errors, warnings };
10995
11232
  }
@@ -11052,21 +11289,33 @@ function applyConfigSet(configPath, rawKey, value) {
11052
11289
  ]
11053
11290
  };
11054
11291
  }
11055
- const file = readConfigFile(configPath);
11056
- if (key === "forgeAuthToken") {
11057
- const account = toStringValue(file.forgeAuthTokenKeychainAccount) || defaultKeychainAccount(configPath);
11058
- setKeychainSecret(account, value);
11059
- const nextFile2 = {
11060
- ...file,
11061
- forgeAuthToken: void 0,
11062
- TESTOPS_FORGE_AUTH_TOKEN: void 0,
11063
- forgeAuthTokenKeychainAccount: account
11064
- };
11065
- writeConfigFile(configPath, nextFile2);
11066
- return {
11067
- exitCode: ExitCode.Success,
11068
- stdout: [`Config updated: forgeAuthToken stored in macOS Keychain (${account}).`]
11069
- };
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";
11306
+ return {
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,
11314
+ stderr: [
11315
+ `Unable to store ${secretKey} securely: ${error instanceof Error ? error.message : "unknown secure-storage error"} Use ${secretEnvironmentName(secretKey)} on Linux/CI.`
11316
+ ]
11317
+ };
11318
+ }
11070
11319
  }
11071
11320
  const nextFile = {
11072
11321
  ...file,
@@ -11078,6 +11327,101 @@ function applyConfigSet(configPath, rawKey, value) {
11078
11327
  stdout: [`Config updated: ${rawKey}.`]
11079
11328
  };
11080
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
+ }
11081
11425
  function createConfigHandler(env = process.env, cwd = process.cwd()) {
11082
11426
  return (request) => {
11083
11427
  const [subcommand, ...subArgs] = request.args;
@@ -11086,14 +11430,22 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
11086
11430
  if (subcommand === "show") {
11087
11431
  return {
11088
11432
  exitCode: ExitCode.Success,
11089
- 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
+ ]
11090
11438
  };
11091
11439
  }
11092
11440
  if (subcommand === "validate") {
11093
11441
  if (validation.ok) {
11094
11442
  return {
11095
11443
  exitCode: ExitCode.Success,
11096
- stdout: ["Config validation: PASS", ...toDisplayLines(resolution), ...validation.warnings.map((line) => `WARN: ${line}`)]
11444
+ stdout: [
11445
+ "Config validation: PASS",
11446
+ ...toDisplayLines(resolution),
11447
+ ...validation.warnings.map((line) => `WARN: ${line}`)
11448
+ ]
11097
11449
  };
11098
11450
  }
11099
11451
  return {
@@ -11103,17 +11455,25 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
11103
11455
  };
11104
11456
  }
11105
11457
  if (subcommand === "set") {
11106
- const [key, ...valueParts] = subArgs;
11107
- 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";
11108
11461
  const value = readValueFromStdin ? (0, import_node_fs.readFileSync)(0, "utf8").trim() : valueParts.join(" ").trim();
11109
11462
  if (!key || !value) {
11110
11463
  return {
11111
11464
  exitCode: ExitCode.UsageError,
11112
- stderr: ["Usage: automatify testops config set <key> <value>"]
11465
+ stderr: ["Usage: automatify testops config set <key> <value|--stdin>"]
11113
11466
  };
11114
11467
  }
11115
11468
  return applyConfigSet(resolution.values.configPath, key, value);
11116
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
+ }
11117
11477
  return {
11118
11478
  exitCode: ExitCode.UsageError,
11119
11479
  stderr: [`Unsupported config subcommand: ${subcommand}`]
@@ -11506,9 +11866,11 @@ function normalizePath(pathValue) {
11506
11866
  return pathValue.replaceAll("\\", "/");
11507
11867
  }
11508
11868
  function discoverFeatureFiles(scannedFiles) {
11509
- const featurePaths = [...new Set(
11510
- scannedFiles.map((filePath) => normalizePath(filePath)).filter((filePath) => filePath.toLowerCase().endsWith(".feature"))
11511
- )].sort((a, b) => a.localeCompare(b));
11869
+ const featurePaths = [
11870
+ ...new Set(
11871
+ scannedFiles.map((filePath) => normalizePath(filePath)).filter((filePath) => filePath.toLowerCase().endsWith(".feature"))
11872
+ )
11873
+ ].sort((a, b) => a.localeCompare(b));
11512
11874
  return {
11513
11875
  total: featurePaths.length,
11514
11876
  paths: featurePaths,
@@ -11747,12 +12109,7 @@ function scanProjectFiles(options) {
11747
12109
  }
11748
12110
 
11749
12111
  // src/auto.ts
11750
- var DETERMINISTIC_STEPS = [
11751
- "detect",
11752
- "discover",
11753
- "map-preview",
11754
- "upload"
11755
- ];
12112
+ var DETERMINISTIC_STEPS = ["detect", "discover", "map-preview", "upload"];
11756
12113
  function parseArgs(args) {
11757
12114
  const flags = {};
11758
12115
  const boolFlags = /* @__PURE__ */ new Set();
@@ -12400,10 +12757,7 @@ function createAutoHandler(deps = {}) {
12400
12757
  const diagnostic = diagMissingProjectContext();
12401
12758
  return {
12402
12759
  exitCode: ExitCode.ValidationError,
12403
- stderr: [
12404
- `ERROR: ${diagnostic.code}: ${diagnostic.message}`,
12405
- `SUGGESTION: ${diagnostic.suggestion}`
12406
- ]
12760
+ stderr: [`ERROR: ${diagnostic.code}: ${diagnostic.message}`, `SUGGESTION: ${diagnostic.suggestion}`]
12407
12761
  };
12408
12762
  }
12409
12763
  execution = await executeUploadFlow({
@@ -13024,17 +13378,19 @@ function createAllureOpenHandler(deps = {}) {
13024
13378
  if (useJson) {
13025
13379
  return {
13026
13380
  exitCode: ExitCode.Success,
13027
- stdout: toJsonLine(buildJsonOutput({
13028
- zipPath,
13029
- dryRun: true,
13030
- served: false,
13031
- opened: false,
13032
- keep,
13033
- extractDir: extractDirRaw,
13034
- rootDir: extractDirRaw,
13035
- indexPath,
13036
- summary
13037
- }))
13381
+ stdout: toJsonLine(
13382
+ buildJsonOutput({
13383
+ zipPath,
13384
+ dryRun: true,
13385
+ served: false,
13386
+ opened: false,
13387
+ keep,
13388
+ extractDir: extractDirRaw,
13389
+ rootDir: extractDirRaw,
13390
+ indexPath,
13391
+ summary
13392
+ })
13393
+ )
13038
13394
  };
13039
13395
  }
13040
13396
  return {
@@ -13084,20 +13440,24 @@ function createAllureOpenHandler(deps = {}) {
13084
13440
  }
13085
13441
  }
13086
13442
  if (useJson) {
13087
- logger.log(JSON.stringify(buildJsonOutput({
13088
- zipPath,
13089
- dryRun: false,
13090
- served: true,
13091
- opened,
13092
- keep,
13093
- extractDir: extractResult.extractDir,
13094
- rootDir: extractResult.rootDir,
13095
- indexPath: extractResult.indexPath,
13096
- url: serverHandle.url,
13097
- port: serverHandle.port,
13098
- host: serverHandle.host,
13099
- summary
13100
- })));
13443
+ logger.log(
13444
+ JSON.stringify(
13445
+ buildJsonOutput({
13446
+ zipPath,
13447
+ dryRun: false,
13448
+ served: true,
13449
+ opened,
13450
+ keep,
13451
+ extractDir: extractResult.extractDir,
13452
+ rootDir: extractResult.rootDir,
13453
+ indexPath: extractResult.indexPath,
13454
+ url: serverHandle.url,
13455
+ port: serverHandle.port,
13456
+ host: serverHandle.host,
13457
+ summary
13458
+ })
13459
+ )
13460
+ );
13101
13461
  } else {
13102
13462
  logger.log(`Allure report extracted to ${extractResult.extractDir}.`);
13103
13463
  if (verbose) {
@@ -13385,13 +13745,15 @@ function createAllureHandler(deps = {}) {
13385
13745
  if (useJson) {
13386
13746
  return {
13387
13747
  exitCode: ExitCode.Success,
13388
- stdout: toJsonLine(buildDownloadJsonOutput({
13389
- issueKey: issueKey2,
13390
- dryRun: true,
13391
- downloaded: false,
13392
- attachment: selected,
13393
- outputPath
13394
- }))
13748
+ stdout: toJsonLine(
13749
+ buildDownloadJsonOutput({
13750
+ issueKey: issueKey2,
13751
+ dryRun: true,
13752
+ downloaded: false,
13753
+ attachment: selected,
13754
+ outputPath
13755
+ })
13756
+ )
13395
13757
  };
13396
13758
  }
13397
13759
  return {
@@ -13418,13 +13780,15 @@ function createAllureHandler(deps = {}) {
13418
13780
  if (useJson) {
13419
13781
  return {
13420
13782
  exitCode: ExitCode.Success,
13421
- stdout: toJsonLine(buildDownloadJsonOutput({
13422
- issueKey: issueKey2,
13423
- dryRun: false,
13424
- downloaded: true,
13425
- attachment: downloaded,
13426
- outputPath
13427
- }))
13783
+ stdout: toJsonLine(
13784
+ buildDownloadJsonOutput({
13785
+ issueKey: issueKey2,
13786
+ dryRun: false,
13787
+ downloaded: true,
13788
+ attachment: downloaded,
13789
+ outputPath
13790
+ })
13791
+ )
13428
13792
  };
13429
13793
  }
13430
13794
  return {
@@ -13472,14 +13836,16 @@ function createAllureHandler(deps = {}) {
13472
13836
  if (useJson) {
13473
13837
  return {
13474
13838
  exitCode: ExitCode.Success,
13475
- stdout: toJsonLine(buildJsonOutput2({
13476
- issueKey,
13477
- uploaded: false,
13478
- commentAdded: false,
13479
- dryRun: true,
13480
- summary,
13481
- attachment: { filename: attachmentFilename }
13482
- }))
13839
+ stdout: toJsonLine(
13840
+ buildJsonOutput2({
13841
+ issueKey,
13842
+ uploaded: false,
13843
+ commentAdded: false,
13844
+ dryRun: true,
13845
+ summary,
13846
+ attachment: { filename: attachmentFilename }
13847
+ })
13848
+ )
13483
13849
  };
13484
13850
  }
13485
13851
  return {
@@ -13515,14 +13881,16 @@ function createAllureHandler(deps = {}) {
13515
13881
  if (useJson) {
13516
13882
  return {
13517
13883
  exitCode: ExitCode.Success,
13518
- stdout: toJsonLine(buildJsonOutput2({
13519
- issueKey,
13520
- uploaded: true,
13521
- commentAdded,
13522
- dryRun: false,
13523
- summary,
13524
- attachment
13525
- }))
13884
+ stdout: toJsonLine(
13885
+ buildJsonOutput2({
13886
+ issueKey,
13887
+ uploaded: true,
13888
+ commentAdded,
13889
+ dryRun: false,
13890
+ summary,
13891
+ attachment
13892
+ })
13893
+ )
13526
13894
  };
13527
13895
  }
13528
13896
  return {
@@ -13545,8 +13913,255 @@ function createAllureHandler(deps = {}) {
13545
13913
  };
13546
13914
  }
13547
13915
 
13916
+ // src/auth.ts
13917
+ var import_node_fs10 = require("node:fs");
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);
13961
+ }
13962
+ return { configArgs, baseUrl, jiraEmail, tokenStdin, unknown };
13963
+ }
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";
13992
+ }
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
+
13548
14163
  // src/bdd.ts
13549
- var import_node_fs10 = require("node:fs");
14164
+ var import_node_fs11 = require("node:fs");
13550
14165
  var import_node_path11 = __toESM(require("node:path"), 1);
13551
14166
  var import_yazl = __toESM(require_yazl(), 1);
13552
14167
 
@@ -13860,9 +14475,11 @@ function parseScenarioFeatureFile(raw, filePath) {
13860
14475
  if (!featureName || !scenarioName || !scenarioKey || steps.length === 0 || scenarioCount !== 1) {
13861
14476
  return null;
13862
14477
  }
13863
- const linkedIssueKeys = [...new Set(
13864
- tags.map((tag) => tag.replace(/^@/, "").trim().toUpperCase()).filter((tag) => ISSUE_KEY_PATTERN.test(tag))
13865
- )];
14478
+ const linkedIssueKeys = [
14479
+ ...new Set(
14480
+ tags.map((tag) => tag.replace(/^@/, "").trim().toUpperCase()).filter((tag) => ISSUE_KEY_PATTERN.test(tag))
14481
+ )
14482
+ ];
13866
14483
  return {
13867
14484
  scenarioKey,
13868
14485
  featureName,
@@ -13969,7 +14586,7 @@ function toJsonExportManifestItems(items) {
13969
14586
  async function defaultCreateZipArchive(archivePath, entries) {
13970
14587
  await new Promise((resolve, reject) => {
13971
14588
  const zip = new import_yazl.ZipFile();
13972
- const output = zip.outputStream.pipe((0, import_node_fs10.createWriteStream)(archivePath));
14589
+ const output = zip.outputStream.pipe((0, import_node_fs11.createWriteStream)(archivePath));
13973
14590
  output.on("close", () => resolve());
13974
14591
  output.on("error", reject);
13975
14592
  zip.outputStream.on("error", reject);
@@ -13999,10 +14616,10 @@ function missingProjectResponse() {
13999
14616
  function createBddHandler(deps = {}) {
14000
14617
  const cwd = deps.cwd ?? process.cwd();
14001
14618
  const env = deps.env ?? process.env;
14002
- const mkdir = deps.mkdir ?? ((targetPath, options) => (0, import_node_fs10.mkdirSync)(targetPath, options));
14003
- const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs10.readFileSync)(filePath, "utf8"));
14004
- const readDir = deps.readDir ?? ((dirPath) => (0, import_node_fs10.readdirSync)(dirPath));
14005
- 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"));
14006
14623
  const createZipArchive = deps.createZipArchive ?? defaultCreateZipArchive;
14007
14624
  return async (request, context) => {
14008
14625
  const [subcommand, ...restArgs] = request.args;
@@ -14060,7 +14677,9 @@ function createBddHandler(deps = {}) {
14060
14677
  stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
14061
14678
  };
14062
14679
  }
14063
- const features = (featureId ? result.data.filter((feature) => feature.id === featureId) : result.data).sort((left, right) => left.payload.name.localeCompare(right.payload.name) || left.createdAt.localeCompare(right.createdAt));
14680
+ const features = (featureId ? result.data.filter((feature) => feature.id === featureId) : result.data).sort(
14681
+ (left, right) => left.payload.name.localeCompare(right.payload.name) || left.createdAt.localeCompare(right.createdAt)
14682
+ );
14064
14683
  if (featureId && features.length === 0) {
14065
14684
  return {
14066
14685
  exitCode: ExitCode.RemoteError,
@@ -14180,7 +14799,9 @@ function createBddHandler(deps = {}) {
14180
14799
  stderr: [`ERROR: ${scenariosResult.error.code}: ${scenariosResult.error.message}`]
14181
14800
  };
14182
14801
  }
14183
- const selectedScenarios = exportAll ? [...scenariosResult.data].sort((left, right) => (left.key ?? left.id).localeCompare(right.key ?? right.id)) : scenariosResult.data.filter(
14802
+ const selectedScenarios = exportAll ? [...scenariosResult.data].sort(
14803
+ (left, right) => (left.key ?? left.id).localeCompare(right.key ?? right.id)
14804
+ ) : scenariosResult.data.filter(
14184
14805
  (item) => item.id === scenarioSelector2 || (item.key ?? "").toUpperCase() === scenarioSelector2.toUpperCase()
14185
14806
  );
14186
14807
  if (!exportAll && selectedScenarios.length === 0) {
@@ -14326,7 +14947,9 @@ function createBddHandler(deps = {}) {
14326
14947
  if (!scenario) {
14327
14948
  return {
14328
14949
  exitCode: ExitCode.RemoteError,
14329
- stderr: [`ERROR: NOT_FOUND: BDD scenario ${parsedScenario.scenarioKey} was not found in this project.`]
14950
+ stderr: [
14951
+ `ERROR: NOT_FOUND: BDD scenario ${parsedScenario.scenarioKey} was not found in this project.`
14952
+ ]
14330
14953
  };
14331
14954
  }
14332
14955
  const nextTags = parsedScenario.tags.filter((tag) => !SCENARIO_METADATA_PATTERN.test(tag));
@@ -14432,7 +15055,9 @@ function createBddHandler(deps = {}) {
14432
15055
  if (!scenarioId && !testCaseKey) {
14433
15056
  return {
14434
15057
  exitCode: ExitCode.ValidationError,
14435
- stderr: ["ERROR: Provide either --id <SCENARIO_ID> or --test-case-key <TC-197> for testops bdd scenarios run."]
15058
+ stderr: [
15059
+ "ERROR: Provide either --id <SCENARIO_ID> or --test-case-key <TC-197> for testops bdd scenarios run."
15060
+ ]
14436
15061
  };
14437
15062
  }
14438
15063
  try {
@@ -14706,10 +15331,7 @@ function listToLines(items) {
14706
15331
  if (items.length === 0) {
14707
15332
  return ["Test cases: 0", "No test cases found for the selected context."];
14708
15333
  }
14709
- return [
14710
- `Test cases: ${items.length}`,
14711
- ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
14712
- ];
15334
+ return [`Test cases: ${items.length}`, ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)];
14713
15335
  }
14714
15336
  function showToLines(testCase) {
14715
15337
  return [
@@ -15009,11 +15631,7 @@ var CONFIG_FLAGS5 = /* @__PURE__ */ new Set([
15009
15631
  "--jira-email",
15010
15632
  "--jira-api-token"
15011
15633
  ]);
15012
- var DIRECT_JIRA_FLAGS = /* @__PURE__ */ new Set([
15013
- "--site",
15014
- "--email",
15015
- "--api-token"
15016
- ]);
15634
+ var DIRECT_JIRA_FLAGS = /* @__PURE__ */ new Set(["--site", "--email", "--api-token"]);
15017
15635
  function parseArgs6(args) {
15018
15636
  const flags = {};
15019
15637
  const boolFlags = /* @__PURE__ */ new Set();
@@ -15094,9 +15712,10 @@ function resolveAllureJiraConfig(parsed, env, cwd) {
15094
15712
  const resolution = resolveCliConfig(pickConfigArgs5(parsed), env, cwd);
15095
15713
  const site = normalizeSite3(parsed.flags["--site"] ?? env.JIRA_SITE ?? resolution.values.baseUrl);
15096
15714
  const email = (parsed.flags["--email"] ?? resolution.values.jiraEmail).trim();
15097
- 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();
15098
15717
  const issueKey = (parsed.flags["--issue-key"] ?? "").trim();
15099
- const errors = [];
15718
+ const errors = directApiToken ? [] : resolution.errors.filter((line) => line.startsWith("jiraApiToken:"));
15100
15719
  if (!hasValue(site)) {
15101
15720
  errors.push("Missing Jira site. Pass --site, set JIRA_SITE/JIRA_BASE_URL, or configure baseUrl.");
15102
15721
  }
@@ -15213,27 +15832,33 @@ function createDoctorHandler(deps = {}) {
15213
15832
  message: "Issue access check skipped. Pass --issue-key to verify Jira read/download access for a specific issue."
15214
15833
  });
15215
15834
  }
15216
- checks.push(commandCheck(
15217
- { execFileSync: runCommand },
15218
- "unzip",
15219
- ["-v"],
15220
- "unzip is available for manual ZIP inspection.",
15221
- "unzip was not found. The future built-in opener can avoid this, but manual inspection will be less convenient."
15222
- ));
15223
- checks.push(commandCheck(
15224
- { execFileSync: runCommand },
15225
- "java",
15226
- ["-version"],
15227
- "Java is available for optional raw allure-results workflows.",
15228
- "Java was not found. This does not block generated Allure HTML ZIP upload/download/open; it only matters for raw allure-results generation."
15229
- ));
15230
- checks.push(commandCheck(
15231
- { execFileSync: runCommand },
15232
- "allure",
15233
- ["--version"],
15234
- "Allure CLI is available for optional raw allure-results workflows.",
15235
- "Allure CLI was not found. This does not block generated Allure HTML ZIP upload/download/open."
15236
- ));
15835
+ checks.push(
15836
+ commandCheck(
15837
+ { execFileSync: runCommand },
15838
+ "unzip",
15839
+ ["-v"],
15840
+ "unzip is available for manual ZIP inspection.",
15841
+ "unzip was not found. The future built-in opener can avoid this, but manual inspection will be less convenient."
15842
+ )
15843
+ );
15844
+ checks.push(
15845
+ commandCheck(
15846
+ { execFileSync: runCommand },
15847
+ "java",
15848
+ ["-version"],
15849
+ "Java is available for optional raw allure-results workflows.",
15850
+ "Java was not found. This does not block generated Allure HTML ZIP upload/download/open; it only matters for raw allure-results generation."
15851
+ )
15852
+ );
15853
+ checks.push(
15854
+ commandCheck(
15855
+ { execFileSync: runCommand },
15856
+ "allure",
15857
+ ["--version"],
15858
+ "Allure CLI is available for optional raw allure-results workflows.",
15859
+ "Allure CLI was not found. This does not block generated Allure HTML ZIP upload/download/open."
15860
+ )
15861
+ );
15237
15862
  const summary2 = {
15238
15863
  status: computeOverallStatus(checks),
15239
15864
  checks
@@ -15272,9 +15897,7 @@ function createDoctorHandler(deps = {}) {
15272
15897
  message: "Forge endpoint is configured."
15273
15898
  });
15274
15899
  }
15275
- const hasContext = Boolean(
15276
- resolution.values.projectKey || resolution.values.issueKey
15277
- );
15900
+ const hasContext = Boolean(resolution.values.projectKey || resolution.values.issueKey);
15278
15901
  if (enforceContextCheck || hasContext) {
15279
15902
  if (!hasContext) {
15280
15903
  checks.push({
@@ -15331,7 +15954,7 @@ function createDoctorHandler(deps = {}) {
15331
15954
  }
15332
15955
 
15333
15956
  // src/ingestFeature.ts
15334
- var import_node_fs11 = require("node:fs");
15957
+ var import_node_fs12 = require("node:fs");
15335
15958
  var import_node_path12 = __toESM(require("node:path"), 1);
15336
15959
  var CONFIG_FLAGS6 = /* @__PURE__ */ new Set([
15337
15960
  "--config",
@@ -15418,8 +16041,8 @@ function normalizeError5(error) {
15418
16041
  return "Unknown ingest error.";
15419
16042
  }
15420
16043
  function createIngestFeatureHandler(deps = {}) {
15421
- const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs11.readFileSync)(filePath, "utf8"));
15422
- 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"));
15423
16046
  const cwd = deps.cwd ?? process.cwd();
15424
16047
  const env = deps.env ?? process.env;
15425
16048
  return async (request, context) => {
@@ -15501,11 +16124,7 @@ function createIngestFeatureHandler(deps = {}) {
15501
16124
  }
15502
16125
  return {
15503
16126
  exitCode: ExitCode.RemoteError,
15504
- stdout: [
15505
- "Feature ingestion: FAILED",
15506
- `Feature: ${name}`,
15507
- `Source: ${useStdin ? "stdin" : sourceFile}`
15508
- ],
16127
+ stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
15509
16128
  stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
15510
16129
  };
15511
16130
  }
@@ -15546,11 +16165,7 @@ function createIngestFeatureHandler(deps = {}) {
15546
16165
  }
15547
16166
  return {
15548
16167
  exitCode: ExitCode.TransportError,
15549
- stdout: [
15550
- "Feature ingestion: FAILED",
15551
- `Feature: ${name}`,
15552
- `Source: ${useStdin ? "stdin" : sourceFile}`
15553
- ],
16168
+ stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
15554
16169
  stderr: [`ERROR: ${normalizeError5(error)}`]
15555
16170
  };
15556
16171
  }
@@ -15558,7 +16173,7 @@ function createIngestFeatureHandler(deps = {}) {
15558
16173
  }
15559
16174
 
15560
16175
  // src/runUpload.ts
15561
- var import_node_fs12 = require("node:fs");
16176
+ var import_node_fs13 = require("node:fs");
15562
16177
  var import_node_path13 = __toESM(require("node:path"), 1);
15563
16178
  var STEP_RESULTS = [
15564
16179
  StepResult.Passed,
@@ -15700,8 +16315,8 @@ function summarizeRunResult(result) {
15700
16315
  return lines;
15701
16316
  }
15702
16317
  function createRunUploadHandler(deps = {}) {
15703
- const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs12.readFileSync)(filePath, "utf8"));
15704
- const readStdin = deps.readStdin ?? (() => (0, import_node_fs12.readFileSync)(0, "utf8"));
16318
+ const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs13.readFileSync)(filePath, "utf8"));
16319
+ const readStdin = deps.readStdin ?? (() => (0, import_node_fs13.readFileSync)(0, "utf8"));
15705
16320
  const cwd = deps.cwd ?? process.cwd();
15706
16321
  const env = deps.env ?? process.env;
15707
16322
  return async (request, context) => {
@@ -16046,7 +16661,7 @@ function createRunsHandler(deps = {}) {
16046
16661
  }
16047
16662
 
16048
16663
  // src/setup.ts
16049
- var import_node_fs14 = require("node:fs");
16664
+ var import_node_fs15 = require("node:fs");
16050
16665
  var import_node_path15 = __toESM(require("node:path"), 1);
16051
16666
 
16052
16667
  // src/githubSetupProvider.ts
@@ -16054,7 +16669,7 @@ var import_node_child_process4 = require("node:child_process");
16054
16669
 
16055
16670
  // src/setupPlan.ts
16056
16671
  var import_node_crypto = require("node:crypto");
16057
- var import_node_fs13 = require("node:fs");
16672
+ var import_node_fs14 = require("node:fs");
16058
16673
  var import_node_path14 = __toESM(require("node:path"), 1);
16059
16674
  var SETUP_PLAN_SCHEMA_VERSION = "automatify.testops.setup/v1";
16060
16675
  var SETUP_PLAN_KIND = "AutomatifyTestOpsSetupPlan";
@@ -16145,18 +16760,12 @@ function buildGitHubSetupPlan(input) {
16145
16760
  const workflowId = requireNonEmpty(input.workflowId, "workflow id");
16146
16761
  const workflowPath = normalizeWorkflowPath(input.workflowPath);
16147
16762
  const profileLabel = requireNonEmpty(input.profileLabel, "profile label");
16148
- const endpointSecretName = normalizeSecretName(
16149
- input.callbackEndpointSecretName,
16150
- "callback endpoint secret name"
16151
- );
16152
- const tokenSecretName = normalizeSecretName(
16153
- input.callbackAuthTokenSecretName,
16154
- "callback auth token secret name"
16155
- );
16763
+ const endpointSecretName = normalizeSecretName(input.callbackEndpointSecretName, "callback endpoint secret name");
16764
+ const tokenSecretName = normalizeSecretName(input.callbackAuthTokenSecretName, "callback auth token secret name");
16156
16765
  if (endpointSecretName === tokenSecretName) {
16157
16766
  throw new Error("callback endpoint and auth token secret names must be different.");
16158
16767
  }
16159
- const workflowContent = (0, import_node_fs13.readFileSync)(input.workflowSourcePath, "utf8");
16768
+ const workflowContent = (0, import_node_fs14.readFileSync)(input.workflowSourcePath, "utf8");
16160
16769
  if (!workflowContent.trim()) {
16161
16770
  throw new Error("workflow source file is empty.");
16162
16771
  }
@@ -16211,13 +16820,7 @@ function buildGitHubSetupPlan(input) {
16211
16820
  "Secrets: read/write metadata only for the explicit github-secrets apply scope.",
16212
16821
  "Contents: read for doctor; this command never creates commits or pull requests."
16213
16822
  ],
16214
- remoteChangesExcluded: [
16215
- "accounts",
16216
- "credentials",
16217
- "commits",
16218
- "pull requests",
16219
- "workflow runs"
16220
- ]
16823
+ remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "workflow runs"]
16221
16824
  }
16222
16825
  },
16223
16826
  jiraProfile: {
@@ -16435,15 +17038,7 @@ function validateSetupPlan(value) {
16435
17038
  errors.push("project.key is required.");
16436
17039
  }
16437
17040
  const github = value.github;
16438
- if (!isRecord(github) || !hasExactKeys(github, [
16439
- "owner",
16440
- "repository",
16441
- "ref",
16442
- "workflowId",
16443
- "workflowFile",
16444
- "requiredSecrets",
16445
- "guidance"
16446
- ])) {
17041
+ if (!isRecord(github) || !hasExactKeys(github, ["owner", "repository", "ref", "workflowId", "workflowFile", "requiredSecrets", "guidance"])) {
16447
17042
  errors.push("github has an invalid shape.");
16448
17043
  } else {
16449
17044
  for (const field of ["owner", "repository", "ref", "workflowId"]) {
@@ -16520,6 +17115,8 @@ function hashSetupContent(value) {
16520
17115
  }
16521
17116
 
16522
17117
  // src/githubSetupProvider.ts
17118
+ var GITHUB_SECRET_PAGE_SIZE = 100;
17119
+ var MAX_GITHUB_SECRET_PAGES = 100;
16523
17120
  function isRecord2(value) {
16524
17121
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16525
17122
  }
@@ -16551,18 +17148,14 @@ async function githubApi(fetchImpl, url, token) {
16551
17148
  }
16552
17149
  async function setGitHubRepositorySecretWithCli(input) {
16553
17150
  await new Promise((resolve, reject) => {
16554
- const child = (0, import_node_child_process4.spawn)(
16555
- "gh",
16556
- ["secret", "set", input.name, "--repo", `${input.owner}/${input.repository}`],
16557
- {
16558
- shell: false,
16559
- stdio: ["pipe", "ignore", "pipe"],
16560
- env: {
16561
- ...process.env,
16562
- GH_TOKEN: input.adminToken
16563
- }
17151
+ const child = (0, import_node_child_process4.spawn)("gh", ["secret", "set", input.name, "--repo", `${input.owner}/${input.repository}`], {
17152
+ shell: false,
17153
+ stdio: ["pipe", "ignore", "pipe"],
17154
+ env: {
17155
+ ...process.env,
17156
+ GH_TOKEN: input.adminToken
16564
17157
  }
16565
- );
17158
+ });
16566
17159
  let stderr = "";
16567
17160
  child.stderr.setEncoding("utf8");
16568
17161
  child.stderr.on("data", (chunk) => {
@@ -16622,6 +17215,7 @@ var GitHubSetupProvider = class {
16622
17215
  return {
16623
17216
  found: true,
16624
17217
  active: state === "active",
17218
+ state,
16625
17219
  path: workflowPath,
16626
17220
  contentSha256,
16627
17221
  message: state !== "active" ? `GitHub workflow is registered but its state is ${state ?? "unknown"}.` : workflowPath !== workflowFile.path ? `GitHub workflow path ${workflowPath ?? "<unknown>"} does not match ${workflowFile.path}.` : contentSha256 === workflowFile.sha256 ? "GitHub workflow is active and its content matches the approved plan." : "GitHub workflow metadata is reachable, but the approved ref content could not be confirmed as an exact plan match."
@@ -16636,25 +17230,52 @@ var GitHubSetupProvider = class {
16636
17230
  };
16637
17231
  }
16638
17232
  const { owner, repository } = plan.github;
16639
- const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/secrets?per_page=100`;
16640
- const response = await githubApi(this.fetchImpl, url, adminToken);
16641
- if (!response.ok) {
16642
- throw new Error(
16643
- `GitHub repository secret metadata request failed with HTTP ${response.status}: ${safeGitHubMessage(response.body, "request failed")}`
16644
- );
16645
- }
17233
+ const baseUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/secrets`;
16646
17234
  const names = /* @__PURE__ */ new Set();
16647
- if (isRecord2(response.body) && Array.isArray(response.body.secrets)) {
16648
- for (const item of response.body.secrets) {
17235
+ let totalCount;
17236
+ let pagesRead = 0;
17237
+ for (let page = 1; page <= MAX_GITHUB_SECRET_PAGES; page += 1) {
17238
+ const url = `${baseUrl}?per_page=${GITHUB_SECRET_PAGE_SIZE}&page=${page}`;
17239
+ const response = await githubApi(this.fetchImpl, url, adminToken);
17240
+ if (!response.ok) {
17241
+ throw new Error(
17242
+ `GitHub repository secret metadata request failed on page ${page} with HTTP ${response.status}: ${safeGitHubMessage(response.body, "request failed")}`
17243
+ );
17244
+ }
17245
+ const body = isRecord2(response.body) ? response.body : {};
17246
+ if (totalCount === void 0 && typeof body.total_count === "number" && Number.isFinite(body.total_count)) {
17247
+ totalCount = Math.max(0, Math.trunc(body.total_count));
17248
+ }
17249
+ const secrets = Array.isArray(body.secrets) ? body.secrets : [];
17250
+ const before = names.size;
17251
+ for (const item of secrets) {
16649
17252
  if (isRecord2(item) && typeof item.name === "string") {
16650
17253
  names.add(item.name);
16651
17254
  }
16652
17255
  }
17256
+ pagesRead = page;
17257
+ if (totalCount !== void 0 && names.size >= totalCount) {
17258
+ break;
17259
+ }
17260
+ if (secrets.length < GITHUB_SECRET_PAGE_SIZE) {
17261
+ if (totalCount !== void 0 && names.size < totalCount) {
17262
+ throw new Error(
17263
+ `GitHub repository secret metadata pagination ended early after ${names.size} of ${totalCount} secret name(s).`
17264
+ );
17265
+ }
17266
+ break;
17267
+ }
17268
+ if (names.size === before) {
17269
+ throw new Error("GitHub repository secret metadata pagination made no progress.");
17270
+ }
17271
+ if (page === MAX_GITHUB_SECRET_PAGES) {
17272
+ throw new Error(`GitHub repository secret metadata exceeded ${MAX_GITHUB_SECRET_PAGES} pages.`);
17273
+ }
16653
17274
  }
16654
17275
  return {
16655
17276
  verified: true,
16656
17277
  names,
16657
- message: `Verified ${names.size} GitHub repository secret name(s); values remain unreadable.`
17278
+ message: `Verified ${names.size} GitHub repository secret name(s) across ${pagesRead} page(s); values remain unreadable.`
16658
17279
  };
16659
17280
  }
16660
17281
  async setCallbackSecret(plan, secret, value, adminToken) {
@@ -16671,7 +17292,12 @@ var GitHubSetupProvider = class {
16671
17292
  // src/azureDevOpsSetupProvider.ts
16672
17293
  var isRecord3 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
16673
17294
  async function adoGet(fetchImpl, url, token) {
16674
- const response = await fetchImpl(url, { headers: { accept: "application/json", ...token ? { authorization: `Basic ${Buffer.from(`:${token}`).toString("base64")}` } : {} } });
17295
+ const response = await fetchImpl(url, {
17296
+ headers: {
17297
+ accept: "application/json",
17298
+ ...token ? { authorization: `Basic ${Buffer.from(`:${token}`).toString("base64")}` } : {}
17299
+ }
17300
+ });
16675
17301
  const text = await response.text();
16676
17302
  let body = {};
16677
17303
  try {
@@ -16684,7 +17310,8 @@ async function setAzurePipelineVariableWithRest(input, fetchImpl) {
16684
17310
  const base = `https://dev.azure.com/${encodeURIComponent(input.organization)}/${encodeURIComponent(input.project)}/_apis/build/definitions/${encodeURIComponent(input.pipelineId)}`;
16685
17311
  const auth = { authorization: `Basic ${Buffer.from(`:${input.token}`).toString("base64")}` };
16686
17312
  const current = await adoGet(fetchImpl, `${base}?api-version=7.1`, input.token);
16687
- if (!current.ok || !isRecord3(current.body)) throw new Error(`Azure DevOps build definition could not be read (HTTP ${current.status}).`);
17313
+ if (!current.ok || !isRecord3(current.body))
17314
+ throw new Error(`Azure DevOps build definition could not be read (HTTP ${current.status}).`);
16688
17315
  const variables = isRecord3(current.body.variables) ? { ...current.body.variables } : {};
16689
17316
  variables[input.name] = { value: input.value, isSecret: true };
16690
17317
  const query = new URLSearchParams({
@@ -16692,9 +17319,25 @@ async function setAzurePipelineVariableWithRest(input, fetchImpl) {
16692
17319
  secretsSourceDefinitionId: String(current.body.id),
16693
17320
  secretsSourceDefinitionRevision: String(current.body.revision)
16694
17321
  });
16695
- const response = await fetchImpl(`${base}?${query.toString()}`, { method: "PUT", headers: { ...auth, "content-type": "application/json", accept: "application/json" }, body: JSON.stringify({ ...current.body, variables }) });
17322
+ const response = await fetchImpl(`${base}?${query.toString()}`, {
17323
+ method: "PUT",
17324
+ headers: { ...auth, "content-type": "application/json", accept: "application/json" },
17325
+ body: JSON.stringify({ ...current.body, variables })
17326
+ });
16696
17327
  if (!response.ok) throw new Error(`Azure DevOps pipeline variable update failed (HTTP ${response.status}).`);
16697
17328
  }
17329
+ function azurePipelineStateMessage(name, queueStatus) {
17330
+ switch (queueStatus) {
17331
+ case "enabled":
17332
+ return `Azure DevOps pipeline ${name} is reachable and enabled. Inspection never queues a run.`;
17333
+ case "paused":
17334
+ return `Azure DevOps pipeline ${name} is reachable but paused; builds may be queued but will not start until the definition is enabled.`;
17335
+ case "disabled":
17336
+ return `Azure DevOps pipeline ${name} is reachable but disabled; new builds cannot be queued until the definition is enabled.`;
17337
+ default:
17338
+ return `Azure DevOps pipeline ${name} is reachable, but its queue status is ${queueStatus ?? "unknown"}.`;
17339
+ }
17340
+ }
16698
17341
  var AzureDevOpsSetupProvider = class {
16699
17342
  name = "azure-devops";
16700
17343
  fetchImpl;
@@ -16707,23 +17350,51 @@ var AzureDevOpsSetupProvider = class {
16707
17350
  const a = plan.azureDevOps;
16708
17351
  const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
16709
17352
  const result = await adoGet(this.fetchImpl, url, token);
16710
- if (!result.ok) return { found: false, message: result.status === 404 ? "Azure DevOps pipeline was not found." : `Azure DevOps pipeline metadata could not be verified (HTTP ${result.status}).` };
17353
+ if (!result.ok)
17354
+ return {
17355
+ found: false,
17356
+ message: result.status === 404 ? "Azure DevOps pipeline was not found." : `Azure DevOps pipeline metadata could not be verified (HTTP ${result.status}).`
17357
+ };
16711
17358
  const body = isRecord3(result.body) ? result.body : {};
16712
- const name = typeof body.name === "string" ? body.name : void 0;
16713
- return { found: true, active: true, path: name, message: `Azure DevOps pipeline ${name ?? a.pipelineId} is reachable. Inspection never queues a run.` };
17359
+ const name = typeof body.name === "string" ? body.name : a.pipelineId;
17360
+ const queueStatus = typeof body.queueStatus === "string" ? body.queueStatus : void 0;
17361
+ return {
17362
+ found: true,
17363
+ active: queueStatus === "enabled",
17364
+ state: queueStatus,
17365
+ path: name,
17366
+ message: azurePipelineStateMessage(name, queueStatus)
17367
+ };
16714
17368
  }
16715
17369
  async inspectCallbackSecretMetadata(plan, token) {
16716
- if (!token) return { verified: false, names: /* @__PURE__ */ new Set(), message: "Azure DevOps pipeline variable metadata was not verified because azureDevOpsAdminToken was not provided. Secret values are never readable." };
17370
+ if (!token)
17371
+ return {
17372
+ verified: false,
17373
+ names: /* @__PURE__ */ new Set(),
17374
+ message: "Azure DevOps pipeline variable metadata was not verified because azureDevOpsAdminToken was not provided. Secret values are never readable."
17375
+ };
16717
17376
  const a = plan.azureDevOps;
16718
17377
  const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
16719
17378
  const result = await adoGet(this.fetchImpl, url, token);
16720
17379
  if (!result.ok) throw new Error(`Azure DevOps pipeline metadata request failed with HTTP ${result.status}.`);
16721
17380
  const variables = isRecord3(result.body) && isRecord3(result.body.variables) ? result.body.variables : {};
16722
- return { verified: true, names: new Set(Object.keys(variables)), message: `Verified ${Object.keys(variables).length} Azure DevOps pipeline variable name(s); secret values remain unreadable.` };
17381
+ return {
17382
+ verified: true,
17383
+ names: new Set(Object.keys(variables)),
17384
+ message: `Verified ${Object.keys(variables).length} Azure DevOps pipeline variable name(s); secret values remain unreadable.`
17385
+ };
16723
17386
  }
16724
17387
  async setCallbackSecret(plan, secret, value, adminToken) {
16725
- if (!this.setter) throw new Error("Azure DevOps pipeline variable setter is not configured; use the safe Azure CLI adapter.");
16726
- await this.setter({ organization: plan.azureDevOps.organization, project: plan.azureDevOps.project, pipelineId: plan.azureDevOps.pipelineId, name: secret.repositorySecretName, value, token: adminToken });
17388
+ if (!this.setter)
17389
+ throw new Error("Azure DevOps pipeline variable setter is not configured; use the safe Azure CLI adapter.");
17390
+ await this.setter({
17391
+ organization: plan.azureDevOps.organization,
17392
+ project: plan.azureDevOps.project,
17393
+ pipelineId: plan.azureDevOps.pipelineId,
17394
+ name: secret.repositorySecretName,
17395
+ value,
17396
+ token: adminToken
17397
+ });
16727
17398
  }
16728
17399
  };
16729
17400
 
@@ -16753,30 +17424,138 @@ function buildAzureDevOpsSetupPlan(input) {
16753
17424
  const endpointName = variableName(input.callbackEndpointVariableName, "callback endpoint variable name");
16754
17425
  const tokenName = variableName(input.callbackAuthTokenVariableName, "callback auth token variable name");
16755
17426
  if (endpointName === tokenName) throw new Error("callback variable names must be distinct.");
16756
- const requiredSecrets = [{ repositorySecretName: endpointName, valueKey: "callbackEndpoint", purpose: "Forge callback endpoint pipeline variable." }, { repositorySecretName: tokenName, valueKey: "callbackAuthToken", purpose: "Forge callback auth token pipeline variable." }];
16757
- const scopes = ["provider-secrets", "jira-profile", ...input.setProjectDefault ? ["project-default"] : []];
16758
- const actions = scopes.map((scope) => ({ id: scope === "provider-secrets" ? "set-provider-secrets" : scope === "jira-profile" ? "upsert-jira-profile" : "set-project-default", scope, summary: `Apply ${scope} for Azure DevOps.`, mutates: "jira" }));
16759
- const withoutId = { schemaVersion: "automatify.testops.setup/v1", kind: "AutomatifyTestOpsSetupPlan", provider: "azure-devops", project: { key: projectKey }, azureDevOps: { organization, project: azureProject, pipelineId, ref, apiVersion, requiredSecrets, guidance: { authentication: "Use an Azure DevOps PAT with least-privilege pipeline read/manage-variable access.", permissions: ["Pipelines: read for doctor; manage variables only for explicit provider-secrets apply."], remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "pipeline runs"] } }, jiraProfile: { label: profileLabel, enabled: input.enabled, provider: "azureDevops", config: { organization, project: azureProject, pipelineId, apiVersion, bodyTemplate: JSON.stringify({ resources: { repositories: { self: { refName: ref } } } }) }, setProjectDefault: input.setProjectDefault }, actions, smokeValidation: [{ id: "pipeline-content", verifies: "Azure DevOps pipeline metadata without queueing a run.", triggersExternalRun: false }, { id: "pipeline-variable-metadata", verifies: "Required variable names; values remain unreadable.", triggersExternalRun: false }, { id: "jira-profile", verifies: "Forge automation profile and optional default.", triggersExternalRun: false }], rollback: actions.map((a) => ({ actionId: a.id, strategy: "Restore the previous state manually; secret values cannot be read back.", automatic: false })), followUp: { azureDevOps: "Azure DevOps setup is implemented through this provider-neutral plan/apply/doctor boundary." } };
17427
+ const requiredSecrets = [
17428
+ {
17429
+ repositorySecretName: endpointName,
17430
+ valueKey: "callbackEndpoint",
17431
+ purpose: "Forge callback endpoint pipeline variable."
17432
+ },
17433
+ {
17434
+ repositorySecretName: tokenName,
17435
+ valueKey: "callbackAuthToken",
17436
+ purpose: "Forge callback auth token pipeline variable."
17437
+ }
17438
+ ];
17439
+ const scopes = [
17440
+ "provider-secrets",
17441
+ "jira-profile",
17442
+ ...input.setProjectDefault ? ["project-default"] : []
17443
+ ];
17444
+ const actions = scopes.map((scope) => ({
17445
+ id: scope === "provider-secrets" ? "set-provider-secrets" : scope === "jira-profile" ? "upsert-jira-profile" : "set-project-default",
17446
+ scope,
17447
+ summary: `Apply ${scope} for Azure DevOps.`,
17448
+ mutates: "jira"
17449
+ }));
17450
+ const withoutId = {
17451
+ schemaVersion: "automatify.testops.setup/v1",
17452
+ kind: "AutomatifyTestOpsSetupPlan",
17453
+ provider: "azure-devops",
17454
+ project: { key: projectKey },
17455
+ azureDevOps: {
17456
+ organization,
17457
+ project: azureProject,
17458
+ pipelineId,
17459
+ ref,
17460
+ apiVersion,
17461
+ requiredSecrets,
17462
+ guidance: {
17463
+ authentication: "Use an Azure DevOps PAT with least-privilege pipeline read/manage-variable access.",
17464
+ permissions: ["Pipelines: read for doctor; manage variables only for explicit provider-secrets apply."],
17465
+ remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "pipeline runs"]
17466
+ }
17467
+ },
17468
+ jiraProfile: {
17469
+ label: profileLabel,
17470
+ enabled: input.enabled,
17471
+ provider: "azureDevops",
17472
+ config: {
17473
+ organization,
17474
+ project: azureProject,
17475
+ pipelineId,
17476
+ apiVersion,
17477
+ bodyTemplate: JSON.stringify({ resources: { repositories: { self: { refName: ref } } } })
17478
+ },
17479
+ setProjectDefault: input.setProjectDefault
17480
+ },
17481
+ actions,
17482
+ smokeValidation: [
17483
+ {
17484
+ id: "pipeline-content",
17485
+ verifies: "Azure DevOps pipeline metadata without queueing a run.",
17486
+ triggersExternalRun: false
17487
+ },
17488
+ {
17489
+ id: "pipeline-variable-metadata",
17490
+ verifies: "Required variable names; values remain unreadable.",
17491
+ triggersExternalRun: false
17492
+ },
17493
+ {
17494
+ id: "jira-profile",
17495
+ verifies: "Forge automation profile and optional default.",
17496
+ triggersExternalRun: false
17497
+ }
17498
+ ],
17499
+ rollback: actions.map((a) => ({
17500
+ actionId: a.id,
17501
+ strategy: "Restore the previous state manually; secret values cannot be read back.",
17502
+ automatic: false
17503
+ })),
17504
+ followUp: {
17505
+ azureDevOps: "Azure DevOps setup is implemented through this provider-neutral plan/apply/doctor boundary."
17506
+ }
17507
+ };
16760
17508
  return { ...withoutId, planId: digest(withoutId) };
16761
17509
  }
16762
17510
  function validateAzureDevOpsSetupPlan(value) {
16763
17511
  const errors = [];
16764
17512
  const record = value;
16765
- const exact = (v, keys) => Boolean(v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).sort().join() === [...keys].sort().join());
16766
- if (!exact(value, ["schemaVersion", "kind", "planId", "provider", "project", "azureDevOps", "jiraProfile", "actions", "smokeValidation", "rollback", "followUp"])) errors.push("Plan has missing or unsupported top-level fields.");
16767
- if (record?.schemaVersion !== "automatify.testops.setup/v1" || record?.kind !== "AutomatifyTestOpsSetupPlan" || record?.provider !== "azure-devops") errors.push("Plan schema, kind, or provider is invalid.");
16768
- if (!exact(record?.project, ["key"]) || !/^[A-Z][A-Z0-9_]{1,31}$/.test(record?.project?.key ?? "")) errors.push("project.key is invalid.");
17513
+ const exact = (v, keys) => Boolean(
17514
+ v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).sort().join() === [...keys].sort().join()
17515
+ );
17516
+ if (!exact(value, [
17517
+ "schemaVersion",
17518
+ "kind",
17519
+ "planId",
17520
+ "provider",
17521
+ "project",
17522
+ "azureDevOps",
17523
+ "jiraProfile",
17524
+ "actions",
17525
+ "smokeValidation",
17526
+ "rollback",
17527
+ "followUp"
17528
+ ]))
17529
+ errors.push("Plan has missing or unsupported top-level fields.");
17530
+ if (record?.schemaVersion !== "automatify.testops.setup/v1" || record?.kind !== "AutomatifyTestOpsSetupPlan" || record?.provider !== "azure-devops")
17531
+ errors.push("Plan schema, kind, or provider is invalid.");
17532
+ if (!exact(record?.project, ["key"]) || !/^[A-Z][A-Z0-9_]{1,31}$/.test(record?.project?.key ?? ""))
17533
+ errors.push("project.key is invalid.");
16769
17534
  const a = record?.azureDevOps;
16770
- if (!exact(a, ["organization", "project", "pipelineId", "ref", "apiVersion", "requiredSecrets", "guidance"]) || ![a?.organization, a?.project, a?.ref, a?.apiVersion].every((x) => typeof x === "string" && x.trim()) || !/^\d+$/.test(a?.pipelineId ?? "")) errors.push("azureDevOps has an invalid shape or fields.");
16771
- if (!Array.isArray(a?.requiredSecrets) || a.requiredSecrets.length !== 2 || new Set(a.requiredSecrets.map((s) => s.repositorySecretName)).size !== 2 || new Set(a.requiredSecrets.map((s) => s.valueKey)).size !== 2 || !a.requiredSecrets.some((s) => s.valueKey === "callbackEndpoint") || !a.requiredSecrets.some((s) => s.valueKey === "callbackAuthToken") || !a.requiredSecrets.every((s) => exact(s, ["repositorySecretName", "valueKey", "purpose"]) && /^[A-Z_][A-Z0-9_]*$/.test(s.repositorySecretName) && s.purpose)) errors.push("azureDevOps.requiredSecrets is invalid.");
17535
+ if (!exact(a, ["organization", "project", "pipelineId", "ref", "apiVersion", "requiredSecrets", "guidance"]) || ![a?.organization, a?.project, a?.ref, a?.apiVersion].every((x) => typeof x === "string" && x.trim()) || !/^\d+$/.test(a?.pipelineId ?? ""))
17536
+ errors.push("azureDevOps has an invalid shape or fields.");
17537
+ if (!Array.isArray(a?.requiredSecrets) || a.requiredSecrets.length !== 2 || new Set(a.requiredSecrets.map((s) => s.repositorySecretName)).size !== 2 || new Set(a.requiredSecrets.map((s) => s.valueKey)).size !== 2 || !a.requiredSecrets.some((s) => s.valueKey === "callbackEndpoint") || !a.requiredSecrets.some((s) => s.valueKey === "callbackAuthToken") || !a.requiredSecrets.every(
17538
+ (s) => exact(s, ["repositorySecretName", "valueKey", "purpose"]) && /^[A-Z_][A-Z0-9_]*$/.test(s.repositorySecretName) && s.purpose
17539
+ ))
17540
+ errors.push("azureDevOps.requiredSecrets is invalid.");
16772
17541
  const p = record?.jiraProfile;
16773
- if (!exact(p, ["label", "enabled", "provider", "config", "setProjectDefault"]) || p?.provider !== "azureDevops" || !exact(p?.config, ["organization", "project", "pipelineId", "apiVersion", "bodyTemplate"]) || p.config.organization !== a?.organization || p.config.project !== a?.project || p.config.pipelineId !== a?.pipelineId || p.config.apiVersion !== a?.apiVersion) errors.push("jiraProfile/config is invalid or inconsistent.");
17542
+ if (!exact(p, ["label", "enabled", "provider", "config", "setProjectDefault"]) || p?.provider !== "azureDevops" || !exact(p?.config, ["organization", "project", "pipelineId", "apiVersion", "bodyTemplate"]) || p.config.organization !== a?.organization || p.config.project !== a?.project || p.config.pipelineId !== a?.pipelineId || p.config.apiVersion !== a?.apiVersion)
17543
+ errors.push("jiraProfile/config is invalid or inconsistent.");
16774
17544
  const ids = p?.setProjectDefault ? ["set-provider-secrets", "upsert-jira-profile", "set-project-default"] : ["set-provider-secrets", "upsert-jira-profile"];
16775
17545
  const scopes = p?.setProjectDefault ? ["provider-secrets", "jira-profile", "project-default"] : ["provider-secrets", "jira-profile"];
16776
- if (!Array.isArray(record?.actions) || record.actions.map((x) => x.id).join() !== ids.join() || !record.actions.every((x, i) => exact(x, ["id", "scope", "summary", "mutates"]) && x.scope === scopes[i] && x.mutates === "jira")) errors.push("actions are invalid.");
17546
+ if (!Array.isArray(record?.actions) || record.actions.map((x) => x.id).join() !== ids.join() || !record.actions.every(
17547
+ (x, i) => exact(x, ["id", "scope", "summary", "mutates"]) && x.scope === scopes[i] && x.mutates === "jira"
17548
+ ))
17549
+ errors.push("actions are invalid.");
16777
17550
  const smokeIds = ["pipeline-content", "pipeline-variable-metadata", "jira-profile"];
16778
- if (!Array.isArray(record?.smokeValidation) || record.smokeValidation.length !== 3 || !record.smokeValidation.every((x, i) => exact(x, ["id", "verifies", "triggersExternalRun"]) && x.id === smokeIds[i] && x.triggersExternalRun === false)) errors.push("smokeValidation is invalid.");
16779
- if (!Array.isArray(record?.rollback) || record.rollback.length !== ids.length || !record.rollback.every((x, i) => exact(x, ["actionId", "strategy", "automatic"]) && x.actionId === ids[i] && x.automatic === false)) errors.push("rollback is invalid.");
17551
+ if (!Array.isArray(record?.smokeValidation) || record.smokeValidation.length !== 3 || !record.smokeValidation.every(
17552
+ (x, i) => exact(x, ["id", "verifies", "triggersExternalRun"]) && x.id === smokeIds[i] && x.triggersExternalRun === false
17553
+ ))
17554
+ errors.push("smokeValidation is invalid.");
17555
+ if (!Array.isArray(record?.rollback) || record.rollback.length !== ids.length || !record.rollback.every(
17556
+ (x, i) => exact(x, ["actionId", "strategy", "automatic"]) && x.actionId === ids[i] && x.automatic === false
17557
+ ))
17558
+ errors.push("rollback is invalid.");
16780
17559
  if (errors.length === 0) {
16781
17560
  const { planId, ...rest } = record;
16782
17561
  if (planId !== digest(rest)) errors.push("planId does not match the plan contents.");
@@ -16932,7 +17711,7 @@ function loadPlan(planPath, cwd) {
16932
17711
  const absolute = import_node_path15.default.resolve(cwd, planPath);
16933
17712
  let parsed;
16934
17713
  try {
16935
- parsed = JSON.parse((0, import_node_fs14.readFileSync)(absolute, "utf8"));
17714
+ parsed = JSON.parse((0, import_node_fs15.readFileSync)(absolute, "utf8"));
16936
17715
  } catch (error) {
16937
17716
  throw new SetupCommandError(
16938
17717
  "PLAN_READ_ERROR",
@@ -16972,14 +17751,26 @@ async function resolveSecrets(parsed, deps) {
16972
17751
  );
16973
17752
  }
16974
17753
  if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
16975
- throw new SetupCommandError("SECRET_INPUT_ERROR", "Secret stdin must be a JSON object.", ExitCode.ValidationError);
17754
+ throw new SetupCommandError(
17755
+ "SECRET_INPUT_ERROR",
17756
+ "Secret stdin must be a JSON object.",
17757
+ ExitCode.ValidationError
17758
+ );
16976
17759
  }
16977
17760
  for (const [key, value] of Object.entries(envelope)) {
16978
17761
  if (!isSecretKey(key)) {
16979
- throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin contains unsupported key ${key}.`, ExitCode.ValidationError);
17762
+ throw new SetupCommandError(
17763
+ "SECRET_INPUT_ERROR",
17764
+ `Secret stdin contains unsupported key ${key}.`,
17765
+ ExitCode.ValidationError
17766
+ );
16980
17767
  }
16981
17768
  if (typeof value !== "string" || !value.trim()) {
16982
- throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin key ${key} must be a non-empty string.`, ExitCode.ValidationError);
17769
+ throw new SetupCommandError(
17770
+ "SECRET_INPUT_ERROR",
17771
+ `Secret stdin key ${key} must be a non-empty string.`,
17772
+ ExitCode.ValidationError
17773
+ );
16983
17774
  }
16984
17775
  values[key] = value.trim();
16985
17776
  sources[key] = "stdin";
@@ -16998,10 +17789,18 @@ async function resolveSecrets(parsed, deps) {
16998
17789
  }
16999
17790
  const envName = target.slice(4);
17000
17791
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) {
17001
- throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret reference for ${key} has an invalid environment variable name.`, ExitCode.ValidationError);
17792
+ throw new SetupCommandError(
17793
+ "SECRET_REFERENCE_ERROR",
17794
+ `Secret reference for ${key} has an invalid environment variable name.`,
17795
+ ExitCode.ValidationError
17796
+ );
17002
17797
  }
17003
17798
  if (sources[key]) {
17004
- throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret ${key} was provided more than once.`, ExitCode.ValidationError);
17799
+ throw new SetupCommandError(
17800
+ "SECRET_REFERENCE_ERROR",
17801
+ `Secret ${key} was provided more than once.`,
17802
+ ExitCode.ValidationError
17803
+ );
17005
17804
  }
17006
17805
  const value = deps.env[envName]?.trim();
17007
17806
  if (!value) {
@@ -17036,7 +17835,11 @@ function approvedScopes(plan, parsed, dryRun) {
17036
17835
  throw new SetupCommandError("APPROVAL_ERROR", `Unsupported approval scope ${scope}.`, ExitCode.UsageError);
17037
17836
  }
17038
17837
  if (!planScopes.includes(scope)) {
17039
- throw new SetupCommandError("APPROVAL_ERROR", `Approval scope ${scope} is not present in this plan.`, ExitCode.UsageError);
17838
+ throw new SetupCommandError(
17839
+ "APPROVAL_ERROR",
17840
+ `Approval scope ${scope} is not present in this plan.`,
17841
+ ExitCode.UsageError
17842
+ );
17040
17843
  }
17041
17844
  expanded.add(scope);
17042
17845
  }
@@ -17109,10 +17912,7 @@ async function prepareState(plan, scopes, secrets, context, deps, dryRun, rotate
17109
17912
  );
17110
17913
  }
17111
17914
  if (secrets.values.githubAdminToken) {
17112
- const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(
17113
- plan,
17114
- secrets.values.githubAdminToken
17115
- );
17915
+ const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(plan, secrets.values.githubAdminToken);
17116
17916
  repositorySecretNames = inspection.names;
17117
17917
  const secretsToWrite = plan.github.requiredSecrets.filter(
17118
17918
  (secret) => rotateSecrets || !repositorySecretNames?.has(secret.repositorySecretName)
@@ -17150,7 +17950,7 @@ function resolveWorkflowTarget(plan, repoRoot) {
17150
17950
  function applyWorkflowFile(plan, repoRoot, dryRun) {
17151
17951
  const action = plan.actions.find((item) => item.scope === "workflow-file");
17152
17952
  const target = resolveWorkflowTarget(plan, repoRoot);
17153
- const sourceContent = (0, import_node_fs14.readFileSync)(plan.github.workflowFile.sourcePath, "utf8");
17953
+ const sourceContent = (0, import_node_fs15.readFileSync)(plan.github.workflowFile.sourcePath, "utf8");
17154
17954
  if (hashSetupContent(sourceContent) !== plan.github.workflowFile.sha256) {
17155
17955
  throw new SetupCommandError(
17156
17956
  "WORKFLOW_SOURCE_CHANGED",
@@ -17158,8 +17958,8 @@ function applyWorkflowFile(plan, repoRoot, dryRun) {
17158
17958
  ExitCode.ValidationError
17159
17959
  );
17160
17960
  }
17161
- const existed = (0, import_node_fs14.existsSync)(target);
17162
- const previousContent = existed ? (0, import_node_fs14.readFileSync)(target, "utf8") : void 0;
17961
+ const existed = (0, import_node_fs15.existsSync)(target);
17962
+ const previousContent = existed ? (0, import_node_fs15.readFileSync)(target, "utf8") : void 0;
17163
17963
  const matches = previousContent !== void 0 && hashSetupContent(previousContent) === plan.github.workflowFile.sha256;
17164
17964
  if (matches) {
17165
17965
  return {
@@ -17171,8 +17971,8 @@ function applyWorkflowFile(plan, repoRoot, dryRun) {
17171
17971
  };
17172
17972
  }
17173
17973
  if (!dryRun) {
17174
- (0, import_node_fs14.mkdirSync)(import_node_path15.default.dirname(target), { recursive: true });
17175
- (0, import_node_fs14.writeFileSync)(target, sourceContent, { encoding: "utf8", mode: 420 });
17974
+ (0, import_node_fs15.mkdirSync)(import_node_path15.default.dirname(target), { recursive: true });
17975
+ (0, import_node_fs15.writeFileSync)(target, sourceContent, { encoding: "utf8", mode: 420 });
17176
17976
  }
17177
17977
  return {
17178
17978
  id: action.id,
@@ -17215,12 +18015,7 @@ async function applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecr
17215
18015
  }
17216
18016
  const adminToken = secrets.values.githubAdminToken;
17217
18017
  for (const secret of pending) {
17218
- await deps.githubProvider.setCallbackSecret(
17219
- plan,
17220
- secret,
17221
- secrets.values[secret.valueKey],
17222
- adminToken
17223
- );
18018
+ await deps.githubProvider.setCallbackSecret(plan, secret, secrets.values[secret.valueKey], adminToken);
17224
18019
  }
17225
18020
  return {
17226
18021
  id: action.id,
@@ -17347,14 +18142,14 @@ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
17347
18142
  const checks = [];
17348
18143
  let forgeTransportFailure = false;
17349
18144
  const target = resolveWorkflowTarget(plan, repoRoot);
17350
- if (!(0, import_node_fs14.existsSync)(target)) {
18145
+ if (!(0, import_node_fs15.existsSync)(target)) {
17351
18146
  checks.push({
17352
18147
  id: "workflow-local",
17353
18148
  status: "fail",
17354
18149
  message: `Local workflow file ${plan.github.workflowFile.path} does not exist.`
17355
18150
  });
17356
18151
  } else {
17357
- const localHash = hashSetupContent((0, import_node_fs14.readFileSync)(target, "utf8"));
18152
+ const localHash = hashSetupContent((0, import_node_fs15.readFileSync)(target, "utf8"));
17358
18153
  checks.push({
17359
18154
  id: "workflow-local",
17360
18155
  status: localHash === plan.github.workflowFile.sha256 ? "pass" : "fail",
@@ -17446,31 +18241,39 @@ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
17446
18241
  function planCommand(args, deps) {
17447
18242
  const parsed = parseArgs10(args, PLAN_VALUE_FLAGS, /* @__PURE__ */ new Set(["--set-default", "--disabled"]));
17448
18243
  if (parsed.errors.length > 0) {
17449
- return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors), ExitCode.UsageError);
18244
+ return jsonResponse(
18245
+ errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors),
18246
+ ExitCode.UsageError
18247
+ );
17450
18248
  }
17451
18249
  const provider = parsed.flags["--provider"] ?? "github-actions";
17452
18250
  if (provider === "azure-devops") {
17453
18251
  try {
17454
- return jsonResponse(buildAzureDevOpsSetupPlan({
17455
- projectKey: parsed.flags["--project-key"] ?? "",
17456
- organization: parsed.flags["--organization"] ?? "",
17457
- azureProject: parsed.flags["--azure-project"] ?? "",
17458
- pipelineId: parsed.flags["--pipeline-id"] ?? "",
17459
- ref: parsed.flags["--ref"] ?? "main",
17460
- apiVersion: parsed.flags["--api-version"],
17461
- profileLabel: parsed.flags["--profile-label"] ?? "Azure DevOps",
17462
- enabled: !parsed.boolFlags.has("--disabled"),
17463
- setProjectDefault: parsed.boolFlags.has("--set-default"),
17464
- callbackEndpointVariableName: parsed.flags["--callback-endpoint-variable-name"] ?? "TESTOPS_FORGE_ENDPOINT",
17465
- callbackAuthTokenVariableName: parsed.flags["--callback-auth-token-variable-name"] ?? "TESTOPS_FORGE_AUTH_TOKEN"
17466
- }));
18252
+ return jsonResponse(
18253
+ buildAzureDevOpsSetupPlan({
18254
+ projectKey: parsed.flags["--project-key"] ?? "",
18255
+ organization: parsed.flags["--organization"] ?? "",
18256
+ azureProject: parsed.flags["--azure-project"] ?? "",
18257
+ pipelineId: parsed.flags["--pipeline-id"] ?? "",
18258
+ ref: parsed.flags["--ref"] ?? "main",
18259
+ apiVersion: parsed.flags["--api-version"],
18260
+ profileLabel: parsed.flags["--profile-label"] ?? "Azure DevOps",
18261
+ enabled: !parsed.boolFlags.has("--disabled"),
18262
+ setProjectDefault: parsed.boolFlags.has("--set-default"),
18263
+ callbackEndpointVariableName: parsed.flags["--callback-endpoint-variable-name"] ?? "TESTOPS_FORGE_ENDPOINT",
18264
+ callbackAuthTokenVariableName: parsed.flags["--callback-auth-token-variable-name"] ?? "TESTOPS_FORGE_AUTH_TOKEN"
18265
+ })
18266
+ );
17467
18267
  } catch (error) {
17468
18268
  return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
17469
18269
  }
17470
18270
  }
17471
18271
  if (provider !== "github-actions") {
17472
18272
  return jsonResponse(
17473
- errorPayload("VALIDATION_ERROR", "Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."),
18273
+ errorPayload(
18274
+ "VALIDATION_ERROR",
18275
+ "Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."
18276
+ ),
17474
18277
  ExitCode.ValidationError
17475
18278
  );
17476
18279
  }
@@ -17491,16 +18294,16 @@ function planCommand(args, deps) {
17491
18294
  });
17492
18295
  return jsonResponse(plan);
17493
18296
  } catch (error) {
17494
- return jsonResponse(
17495
- errorPayload("VALIDATION_ERROR", safeErrorMessage(error)),
17496
- ExitCode.ValidationError
17497
- );
18297
+ return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
17498
18298
  }
17499
18299
  }
17500
18300
  async function applyCommand(args, context, deps) {
17501
18301
  const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, APPLY_BOOL_FLAGS, true);
17502
18302
  if (parsed.errors.length > 0) {
17503
- return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors), ExitCode.UsageError);
18303
+ return jsonResponse(
18304
+ errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors),
18305
+ ExitCode.UsageError
18306
+ );
17504
18307
  }
17505
18308
  const planPath = parsed.flags["--plan"];
17506
18309
  if (!planPath) {
@@ -17525,16 +18328,7 @@ async function applyCommand(args, context, deps) {
17525
18328
  const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
17526
18329
  const rotateSecrets = parsed.boolFlags.has("--rotate-secrets");
17527
18330
  const rotateProviderToken = parsed.boolFlags.has("--rotate-provider-token");
17528
- const state = await prepareState(
17529
- plan,
17530
- scopes,
17531
- secrets,
17532
- context,
17533
- deps,
17534
- dryRun,
17535
- rotateSecrets,
17536
- rotateProviderToken
17537
- );
18331
+ const state = await prepareState(plan, scopes, secrets, context, deps, dryRun, rotateSecrets, rotateProviderToken);
17538
18332
  let appliedProfile = state.matchingProfile;
17539
18333
  for (const action of plan.actions) {
17540
18334
  if (!scopes.includes(action.scope)) {
@@ -17553,14 +18347,7 @@ async function applyCommand(args, context, deps) {
17553
18347
  } else if (action.scope === "github-secrets") {
17554
18348
  actionResults.push(await applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets));
17555
18349
  } else if (action.scope === "jira-profile") {
17556
- const applied = await applyJiraProfile(
17557
- plan,
17558
- state,
17559
- secrets,
17560
- context,
17561
- dryRun,
17562
- rotateProviderToken
17563
- );
18350
+ const applied = await applyJiraProfile(plan, state, secrets, context, dryRun, rotateProviderToken);
17564
18351
  appliedProfile = applied.profile;
17565
18352
  actionResults.push(applied.result);
17566
18353
  } else if (action.scope === "project-default") {
@@ -17623,49 +18410,146 @@ async function applyCommand(args, context, deps) {
17623
18410
  async function applyAzureCommand(plan, parsed, context, deps, secrets) {
17624
18411
  const dryRun = parsed.boolFlags.has("--dry-run");
17625
18412
  const scopes = approvedScopes(plan, parsed, dryRun);
17626
- const profiles = requireServiceData(await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }), "listScenarioAutomationProfiles");
18413
+ const profiles = requireServiceData(
18414
+ await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }),
18415
+ "listScenarioAutomationProfiles"
18416
+ );
17627
18417
  const current = profiles.find((item) => item.label === plan.jiraProfile.label);
17628
- if (!dryRun && scopes.includes("provider-secrets") && !secrets.values.azureDevOpsAdminToken) throw new SetupCommandError("SECRET_INPUT_REQUIRED", "azureDevOpsAdminToken is required for the approved Azure DevOps pipeline-variable scope.", ExitCode.ValidationError);
18418
+ if (!dryRun && scopes.includes("provider-secrets") && !secrets.values.azureDevOpsAdminToken)
18419
+ throw new SetupCommandError(
18420
+ "SECRET_INPUT_REQUIRED",
18421
+ "azureDevOpsAdminToken is required for the approved Azure DevOps pipeline-variable scope.",
18422
+ ExitCode.ValidationError
18423
+ );
17629
18424
  const results = [];
17630
18425
  const secretNames = scopes.includes("provider-secrets") && secrets.values.azureDevOpsAdminToken ? (await deps.azureProvider.inspectCallbackSecretMetadata(plan, secrets.values.azureDevOpsAdminToken)).names : void 0;
17631
18426
  let appliedProfile = current;
17632
18427
  for (const action of plan.actions) {
17633
18428
  if (!scopes.includes(action.scope)) {
17634
- results.push({ id: action.id, scope: action.scope, status: "skipped", message: "Action was not in the explicit approval scope.", rollback: { available: false, guidance: "No mutation occurred." } });
18429
+ results.push({
18430
+ id: action.id,
18431
+ scope: action.scope,
18432
+ status: "skipped",
18433
+ message: "Action was not in the explicit approval scope.",
18434
+ rollback: { available: false, guidance: "No mutation occurred." }
18435
+ });
17635
18436
  continue;
17636
18437
  }
17637
18438
  if (action.scope === "provider-secrets") {
17638
- const pending = plan.azureDevOps.requiredSecrets.filter((item) => parsed.boolFlags.has("--rotate-secrets") || !secretNames?.has(item.repositorySecretName));
17639
- if (!dryRun && pending.some((item) => !secrets.values[item.valueKey])) throw new SetupCommandError("SECRET_INPUT_REQUIRED", "Azure DevOps callback secrets are required for approved pipeline variable changes.", ExitCode.ValidationError);
17640
- if (!dryRun) for (const item of pending) await deps.azureProvider.setCallbackSecret(plan, item, secrets.values[item.valueKey], secrets.values.azureDevOpsAdminToken);
17641
- results.push({ id: action.id, scope: action.scope, status: dryRun ? "planned" : pending.length ? "updated" : "skipped", message: "Azure DevOps pipeline variable names are managed without printing secret values.", rollback: { available: false, guidance: rollbackFor(plan, action.id) } });
18439
+ const pending = plan.azureDevOps.requiredSecrets.filter(
18440
+ (item) => parsed.boolFlags.has("--rotate-secrets") || !secretNames?.has(item.repositorySecretName)
18441
+ );
18442
+ if (!dryRun && pending.some((item) => !secrets.values[item.valueKey]))
18443
+ throw new SetupCommandError(
18444
+ "SECRET_INPUT_REQUIRED",
18445
+ "Azure DevOps callback secrets are required for approved pipeline variable changes.",
18446
+ ExitCode.ValidationError
18447
+ );
18448
+ if (!dryRun)
18449
+ for (const item of pending)
18450
+ await deps.azureProvider.setCallbackSecret(
18451
+ plan,
18452
+ item,
18453
+ secrets.values[item.valueKey],
18454
+ secrets.values.azureDevOpsAdminToken
18455
+ );
18456
+ results.push({
18457
+ id: action.id,
18458
+ scope: action.scope,
18459
+ status: dryRun ? "planned" : pending.length ? "updated" : "skipped",
18460
+ message: "Azure DevOps pipeline variable names are managed without printing secret values.",
18461
+ rollback: { available: false, guidance: rollbackFor(plan, action.id) }
18462
+ });
17642
18463
  } else if (action.scope === "jira-profile") {
17643
18464
  const rotateProvider = parsed.boolFlags.has("--rotate-provider-token");
17644
- if (!dryRun && (!current?.hasSecret || rotateProvider) && !secrets.values.azureDevOpsProviderToken) throw new SetupCommandError("SECRET_INPUT_REQUIRED", "azureDevOpsProviderToken is required to create or rotate the Azure provider PAT.", ExitCode.ValidationError);
17645
- const profile = dryRun ? current ?? { id: "<created-by-forge>", projectKey: plan.project.key, label: plan.jiraProfile.label, provider: "azureDevops", authType: "basicPat", method: "POST", endpointSummary: "<computed-by-forge>", config: plan.jiraProfile.config, enabled: plan.jiraProfile.enabled, hasSecret: false, createdAt: "", updatedAt: "" } : requireServiceData(await context.invokeForgeContract("upsertScenarioAutomationProfile", { context: { projectKey: plan.project.key }, input: { profileId: current?.id, label: plan.jiraProfile.label, provider: "azureDevops", config: plan.jiraProfile.config, enabled: plan.jiraProfile.enabled, secretToken: !current?.hasSecret || rotateProvider ? secrets.values.azureDevOpsProviderToken : void 0 } }), "upsertScenarioAutomationProfile");
18465
+ if (!dryRun && (!current?.hasSecret || rotateProvider) && !secrets.values.azureDevOpsProviderToken)
18466
+ throw new SetupCommandError(
18467
+ "SECRET_INPUT_REQUIRED",
18468
+ "azureDevOpsProviderToken is required to create or rotate the Azure provider PAT.",
18469
+ ExitCode.ValidationError
18470
+ );
18471
+ const profile = dryRun ? current ?? {
18472
+ id: "<created-by-forge>",
18473
+ projectKey: plan.project.key,
18474
+ label: plan.jiraProfile.label,
18475
+ provider: "azureDevops",
18476
+ authType: "basicPat",
18477
+ method: "POST",
18478
+ endpointSummary: "<computed-by-forge>",
18479
+ config: plan.jiraProfile.config,
18480
+ enabled: plan.jiraProfile.enabled,
18481
+ hasSecret: false,
18482
+ createdAt: "",
18483
+ updatedAt: ""
18484
+ } : requireServiceData(
18485
+ await context.invokeForgeContract("upsertScenarioAutomationProfile", {
18486
+ context: { projectKey: plan.project.key },
18487
+ input: {
18488
+ profileId: current?.id,
18489
+ label: plan.jiraProfile.label,
18490
+ provider: "azureDevops",
18491
+ config: plan.jiraProfile.config,
18492
+ enabled: plan.jiraProfile.enabled,
18493
+ secretToken: !current?.hasSecret || rotateProvider ? secrets.values.azureDevOpsProviderToken : void 0
18494
+ }
18495
+ }),
18496
+ "upsertScenarioAutomationProfile"
18497
+ );
17646
18498
  appliedProfile = profile;
17647
- results.push({ id: action.id, scope: action.scope, status: dryRun ? "planned" : current ? "updated" : "created", message: "Azure DevOps Jira automation profile applied through the existing Forge contract.", rollback: { available: !dryRun, guidance: rollbackFor(plan, action.id) } });
18499
+ results.push({
18500
+ id: action.id,
18501
+ scope: action.scope,
18502
+ status: dryRun ? "planned" : current ? "updated" : "created",
18503
+ message: "Azure DevOps Jira automation profile applied through the existing Forge contract.",
18504
+ rollback: { available: !dryRun, guidance: rollbackFor(plan, action.id) }
18505
+ });
17648
18506
  } else if (action.scope === "project-default" && appliedProfile && !dryRun) {
17649
- requireServiceData(await context.invokeForgeContract("setScenarioAutomationDefaultProfile", { context: { projectKey: plan.project.key }, profileId: appliedProfile.id }), "setScenarioAutomationDefaultProfile");
17650
- results.push({ id: action.id, scope: action.scope, status: "updated", message: "Set the Azure DevOps Jira automation profile as project default.", rollback: { available: true, guidance: rollbackFor(plan, action.id) } });
18507
+ requireServiceData(
18508
+ await context.invokeForgeContract("setScenarioAutomationDefaultProfile", {
18509
+ context: { projectKey: plan.project.key },
18510
+ profileId: appliedProfile.id
18511
+ }),
18512
+ "setScenarioAutomationDefaultProfile"
18513
+ );
18514
+ results.push({
18515
+ id: action.id,
18516
+ scope: action.scope,
18517
+ status: "updated",
18518
+ message: "Set the Azure DevOps Jira automation profile as project default.",
18519
+ rollback: { available: true, guidance: rollbackFor(plan, action.id) }
18520
+ });
17651
18521
  } else if (action.scope === "project-default") {
17652
- results.push({ id: action.id, scope: action.scope, status: "planned", message: "Set the Azure DevOps Jira automation profile as project default.", rollback: { available: false, guidance: rollbackFor(plan, action.id) } });
18522
+ results.push({
18523
+ id: action.id,
18524
+ scope: action.scope,
18525
+ status: "planned",
18526
+ message: "Set the Azure DevOps Jira automation profile as project default.",
18527
+ rollback: { available: false, guidance: rollbackFor(plan, action.id) }
18528
+ });
17653
18529
  }
17654
18530
  }
17655
- return jsonResponse({ schemaVersion: APPLY_RESULT_SCHEMA_VERSION, planId: plan.planId, provider: plan.provider, status: dryRun ? "dry-run" : "applied", dryRun, confirmed: !dryRun, approvedScopes: scopes, actions: results, smokeValidation: { status: "not-run", externalRunTriggered: false } });
18531
+ return jsonResponse({
18532
+ schemaVersion: APPLY_RESULT_SCHEMA_VERSION,
18533
+ planId: plan.planId,
18534
+ provider: plan.provider,
18535
+ status: dryRun ? "dry-run" : "applied",
18536
+ dryRun,
18537
+ confirmed: !dryRun,
18538
+ approvedScopes: scopes,
18539
+ actions: results,
18540
+ smokeValidation: { status: "not-run", externalRunTriggered: false }
18541
+ });
17656
18542
  }
17657
18543
  async function doctorCommand(args, context, deps) {
17658
- const parsed = parseArgs10(
17659
- args,
17660
- APPLY_VALUE_FLAGS,
17661
- /* @__PURE__ */ new Set(["--secrets-stdin"]),
17662
- true
17663
- );
18544
+ const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, /* @__PURE__ */ new Set(["--secrets-stdin"]), true);
17664
18545
  if (parsed.approvals.length > 0) {
17665
18546
  return jsonResponse(errorPayload("USAGE_ERROR", "setup doctor does not accept --approve."), ExitCode.UsageError);
17666
18547
  }
17667
18548
  if (parsed.errors.length > 0) {
17668
- return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors), ExitCode.UsageError);
18549
+ return jsonResponse(
18550
+ errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors),
18551
+ ExitCode.UsageError
18552
+ );
17669
18553
  }
17670
18554
  const planPath = parsed.flags["--plan"];
17671
18555
  if (!planPath) {
@@ -17677,19 +18561,61 @@ async function doctorCommand(args, context, deps) {
17677
18561
  secrets = await resolveSecrets(parsed, deps);
17678
18562
  if (plan.provider === "azure-devops") {
17679
18563
  const checks = [];
17680
- const inspection = await deps.azureProvider.inspectExecutionDefinition(plan, secrets.values.azureDevOpsProviderToken ?? secrets.values.azureDevOpsAdminToken);
17681
- checks.push({ id: "pipeline-azure", status: inspection.found ? "pass" : "fail", message: inspection.message });
17682
- const variables = await deps.azureProvider.inspectCallbackSecretMetadata(plan, secrets.values.azureDevOpsAdminToken);
17683
- const missing = plan.azureDevOps.requiredSecrets.filter((item) => !variables.names.has(item.repositorySecretName));
17684
- checks.push({ id: "callback-secret-metadata", status: variables.verified && missing.length === 0 ? "pass" : variables.verified ? "fail" : "warn", message: variables.message });
17685
- const profiles = requireServiceData(await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }), "listScenarioAutomationProfiles");
18564
+ const inspection = await deps.azureProvider.inspectExecutionDefinition(
18565
+ plan,
18566
+ secrets.values.azureDevOpsProviderToken ?? secrets.values.azureDevOpsAdminToken
18567
+ );
18568
+ checks.push({
18569
+ id: "pipeline-azure",
18570
+ status: inspection.found && inspection.active === true ? "pass" : "fail",
18571
+ message: inspection.message
18572
+ });
18573
+ const variables = await deps.azureProvider.inspectCallbackSecretMetadata(
18574
+ plan,
18575
+ secrets.values.azureDevOpsAdminToken
18576
+ );
18577
+ const missing = plan.azureDevOps.requiredSecrets.filter(
18578
+ (item) => !variables.names.has(item.repositorySecretName)
18579
+ );
18580
+ checks.push({
18581
+ id: "callback-secret-metadata",
18582
+ status: variables.verified && missing.length === 0 ? "pass" : variables.verified ? "fail" : "warn",
18583
+ message: variables.message
18584
+ });
18585
+ const profiles = requireServiceData(
18586
+ await context.invokeForgeContract("listScenarioAutomationProfiles", {
18587
+ context: { projectKey: plan.project.key }
18588
+ }),
18589
+ "listScenarioAutomationProfiles"
18590
+ );
17686
18591
  const profile = profiles.find((item) => item.label === plan.jiraProfile.label);
17687
18592
  const config = profile?.config;
17688
18593
  const matches = profile?.provider === "azureDevops" && profile.enabled === plan.jiraProfile.enabled && profile.hasSecret && config?.organization === plan.jiraProfile.config.organization && config?.project === plan.jiraProfile.config.project && config?.pipelineId === plan.jiraProfile.config.pipelineId && config?.apiVersion === plan.jiraProfile.config.apiVersion && config?.bodyTemplate === plan.jiraProfile.config.bodyTemplate;
17689
- checks.push({ id: "jira-profile", status: matches ? "pass" : "fail", message: matches ? "Jira automation profile fields match and Forge reports a stored provider secret." : "Jira automation profile is missing, differs, or lacks provider secret metadata." });
17690
- if (plan.jiraProfile.setProjectDefault) checks.push({ id: "jira-project-default", status: profile?.isProjectDefault ? "pass" : "fail", message: profile?.isProjectDefault ? "Jira automation profile is the project default." : "Jira automation profile is not the project default." });
18594
+ checks.push({
18595
+ id: "jira-profile",
18596
+ status: matches ? "pass" : "fail",
18597
+ message: matches ? "Jira automation profile fields match and Forge reports a stored provider secret." : "Jira automation profile is missing, differs, or lacks provider secret metadata."
18598
+ });
18599
+ if (plan.jiraProfile.setProjectDefault)
18600
+ checks.push({
18601
+ id: "jira-project-default",
18602
+ status: profile?.isProjectDefault ? "pass" : "fail",
18603
+ message: profile?.isProjectDefault ? "Jira automation profile is the project default." : "Jira automation profile is not the project default."
18604
+ });
17691
18605
  const status = doctorStatus(checks);
17692
- return jsonResponse({ schemaVersion: DOCTOR_RESULT_SCHEMA_VERSION, planId: plan.planId, provider: plan.provider, status, exitCode: status === "fail" ? ExitCode.ValidationError : ExitCode.Success, externalRunTriggered: false, checks });
18606
+ const exitCode = status === "fail" ? ExitCode.ValidationError : ExitCode.Success;
18607
+ return jsonResponse(
18608
+ {
18609
+ schemaVersion: DOCTOR_RESULT_SCHEMA_VERSION,
18610
+ planId: plan.planId,
18611
+ provider: plan.provider,
18612
+ status,
18613
+ exitCode,
18614
+ externalRunTriggered: false,
18615
+ checks
18616
+ },
18617
+ exitCode
18618
+ );
17693
18619
  }
17694
18620
  const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
17695
18621
  const summary = await inspectPlan(plan, repoRoot, secrets, context, deps);
@@ -17811,10 +18737,7 @@ function suitesToLines(items) {
17811
18737
  if (items.length === 0) {
17812
18738
  return ["Test suites: 0", "No test suites found for the selected context."];
17813
18739
  }
17814
- return [
17815
- `Test suites: ${items.length}`,
17816
- ...items.map((item) => `- ${item.key} ${item.name}`)
17817
- ];
18740
+ return [`Test suites: ${items.length}`, ...items.map((item) => `- ${item.key} ${item.name}`)];
17818
18741
  }
17819
18742
  function suiteToLines(suite) {
17820
18743
  return [
@@ -18237,6 +19160,12 @@ var COMMAND_REGISTRY = [
18237
19160
  subcommands: ["upload", "download", "open"],
18238
19161
  handler: createAllureHandler()
18239
19162
  },
19163
+ {
19164
+ name: "auth",
19165
+ description: "Secret-safe Jira authentication login, status, and logout commands",
19166
+ subcommands: ["login", "status", "logout"],
19167
+ handler: createAuthHandler()
19168
+ },
18240
19169
  {
18241
19170
  name: "auto",
18242
19171
  description: "Smart auto-ingestion command contract (MVP staged)",
@@ -18259,7 +19188,7 @@ var COMMAND_REGISTRY = [
18259
19188
  {
18260
19189
  name: "config",
18261
19190
  description: "Configuration and auth bootstrap commands",
18262
- subcommands: ["show", "validate", "set"],
19191
+ subcommands: ["show", "validate", "set", "unset", "migrate-secrets"],
18263
19192
  handler: createConfigHandler()
18264
19193
  },
18265
19194
  {
@@ -18381,9 +19310,7 @@ function buildHelpText(registry = COMMAND_REGISTRY, experimentalSyncEnabled = fa
18381
19310
  "Available command groups:"
18382
19311
  ];
18383
19312
  for (const command of visibleCommands) {
18384
- lines.push(
18385
- ` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`
18386
- );
19313
+ lines.push(` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`);
18387
19314
  }
18388
19315
  lines.push(
18389
19316
  "",