@automatify-au/cli 0.1.12 → 0.1.14

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.
@@ -10817,7 +10817,9 @@ function readConfigFile(configPath) {
10817
10817
  function writeConfigFile(configPath, file) {
10818
10818
  (0, import_node_fs.writeFileSync)(configPath, `${JSON.stringify(file, null, 2)}
10819
10819
  `, "utf8");
10820
- (0, import_node_fs.chmodSync)(configPath, 384);
10820
+ if (process.platform !== "win32") {
10821
+ (0, import_node_fs.chmodSync)(configPath, 384);
10822
+ }
10821
10823
  }
10822
10824
  function normalizeBaseUrl(value) {
10823
10825
  if (value.endsWith("/")) {
@@ -10852,6 +10854,41 @@ function setKeychainSecret(account, value) {
10852
10854
  stdio: ["ignore", "ignore", "pipe"]
10853
10855
  });
10854
10856
  }
10857
+ function protectWindowsSecret(value) {
10858
+ const script = [
10859
+ "Add-Type -AssemblyName System.Security",
10860
+ "$plain = [Console]::In.ReadToEnd()",
10861
+ "$bytes = [Text.Encoding]::UTF8.GetBytes($plain)",
10862
+ "$cipher = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
10863
+ "[Console]::Out.Write([Convert]::ToBase64String($cipher))"
10864
+ ].join("; ");
10865
+ return (0, import_node_child_process.execFileSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
10866
+ input: value,
10867
+ encoding: "utf8",
10868
+ stdio: ["pipe", "pipe", "pipe"]
10869
+ }).trim();
10870
+ }
10871
+ function unprotectWindowsSecret(protectedValue) {
10872
+ if (!protectedValue || process.platform !== "win32") {
10873
+ return "";
10874
+ }
10875
+ const script = [
10876
+ "Add-Type -AssemblyName System.Security",
10877
+ "$encoded = [Console]::In.ReadToEnd()",
10878
+ "$cipher = [Convert]::FromBase64String($encoded)",
10879
+ "$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($cipher, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
10880
+ "[Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))"
10881
+ ].join("; ");
10882
+ try {
10883
+ return (0, import_node_child_process.execFileSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
10884
+ input: protectedValue,
10885
+ encoding: "utf8",
10886
+ stdio: ["pipe", "pipe", "ignore"]
10887
+ }).trim();
10888
+ } catch {
10889
+ return "";
10890
+ }
10891
+ }
10855
10892
  function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "") {
10856
10893
  if (flagValue) {
10857
10894
  return { value: flagValue, source: "flag" };
@@ -10926,7 +10963,8 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
10926
10963
  const forgeAuthTokenFromEnv = toStringValue(env.TESTOPS_FORGE_AUTH_TOKEN);
10927
10964
  const forgeAuthTokenFromFile = toStringValue(file.forgeAuthToken ?? file.TESTOPS_FORGE_AUTH_TOKEN);
10928
10965
  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" };
10966
+ const forgeAuthTokenFromWindowsStore = forgeAuthTokenFromEnv || forgeAuthTokenFromFile || forgeAuthTokenFromKeychain ? "" : unprotectWindowsSecret(toStringValue(file.forgeAuthTokenProtected));
10967
+ const forgeAuthTokenResolved = forgeAuthTokenFromEnv ? { value: forgeAuthTokenFromEnv, source: "env" } : forgeAuthTokenFromFile ? { value: forgeAuthTokenFromFile, source: "file" } : forgeAuthTokenFromKeychain ? { value: forgeAuthTokenFromKeychain, source: "keychain" } : forgeAuthTokenFromWindowsStore ? { value: forgeAuthTokenFromWindowsStore, source: "secure-store" } : { value: "", source: "default" };
10930
10968
  const config = {
10931
10969
  baseUrl: normalizeBaseUrl(baseUrlResolved.value),
10932
10970
  projectKey: projectKeyResolved.value,
@@ -11054,12 +11092,44 @@ function applyConfigSet(configPath, rawKey, value) {
11054
11092
  }
11055
11093
  const file = readConfigFile(configPath);
11056
11094
  if (key === "forgeAuthToken") {
11095
+ if (process.platform === "win32") {
11096
+ try {
11097
+ const protectedValue = protectWindowsSecret(value);
11098
+ writeConfigFile(configPath, {
11099
+ ...file,
11100
+ forgeAuthToken: void 0,
11101
+ TESTOPS_FORGE_AUTH_TOKEN: void 0,
11102
+ forgeAuthTokenKeychainAccount: void 0,
11103
+ forgeAuthTokenProtected: protectedValue
11104
+ });
11105
+ return {
11106
+ exitCode: ExitCode.Success,
11107
+ stdout: ["Config updated: forgeAuthToken protected with Windows DPAPI for the current user."]
11108
+ };
11109
+ } catch (error) {
11110
+ return {
11111
+ exitCode: ExitCode.ValidationError,
11112
+ stderr: [
11113
+ `Unable to store forgeAuthToken securely: ${error instanceof Error ? error.message : "unknown secure-storage error"}`
11114
+ ]
11115
+ };
11116
+ }
11117
+ }
11118
+ if (process.platform !== "darwin") {
11119
+ return {
11120
+ exitCode: ExitCode.UsageError,
11121
+ stderr: [
11122
+ "Secure local forgeAuthToken storage is currently supported on macOS and Windows. On Linux/CI, set TESTOPS_FORGE_AUTH_TOKEN in the environment."
11123
+ ]
11124
+ };
11125
+ }
11057
11126
  const account = toStringValue(file.forgeAuthTokenKeychainAccount) || defaultKeychainAccount(configPath);
11058
11127
  setKeychainSecret(account, value);
11059
11128
  const nextFile2 = {
11060
11129
  ...file,
11061
11130
  forgeAuthToken: void 0,
11062
11131
  TESTOPS_FORGE_AUTH_TOKEN: void 0,
11132
+ forgeAuthTokenProtected: void 0,
11063
11133
  forgeAuthTokenKeychainAccount: account
11064
11134
  };
11065
11135
  writeConfigFile(configPath, nextFile2);
@@ -11093,7 +11163,11 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
11093
11163
  if (validation.ok) {
11094
11164
  return {
11095
11165
  exitCode: ExitCode.Success,
11096
- stdout: ["Config validation: PASS", ...toDisplayLines(resolution), ...validation.warnings.map((line) => `WARN: ${line}`)]
11166
+ stdout: [
11167
+ "Config validation: PASS",
11168
+ ...toDisplayLines(resolution),
11169
+ ...validation.warnings.map((line) => `WARN: ${line}`)
11170
+ ]
11097
11171
  };
11098
11172
  }
11099
11173
  return {
@@ -11506,9 +11580,11 @@ function normalizePath(pathValue) {
11506
11580
  return pathValue.replaceAll("\\", "/");
11507
11581
  }
11508
11582
  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));
11583
+ const featurePaths = [
11584
+ ...new Set(
11585
+ scannedFiles.map((filePath) => normalizePath(filePath)).filter((filePath) => filePath.toLowerCase().endsWith(".feature"))
11586
+ )
11587
+ ].sort((a, b) => a.localeCompare(b));
11512
11588
  return {
11513
11589
  total: featurePaths.length,
11514
11590
  paths: featurePaths,
@@ -11747,12 +11823,7 @@ function scanProjectFiles(options) {
11747
11823
  }
11748
11824
 
11749
11825
  // src/auto.ts
11750
- var DETERMINISTIC_STEPS = [
11751
- "detect",
11752
- "discover",
11753
- "map-preview",
11754
- "upload"
11755
- ];
11826
+ var DETERMINISTIC_STEPS = ["detect", "discover", "map-preview", "upload"];
11756
11827
  function parseArgs(args) {
11757
11828
  const flags = {};
11758
11829
  const boolFlags = /* @__PURE__ */ new Set();
@@ -12400,10 +12471,7 @@ function createAutoHandler(deps = {}) {
12400
12471
  const diagnostic = diagMissingProjectContext();
12401
12472
  return {
12402
12473
  exitCode: ExitCode.ValidationError,
12403
- stderr: [
12404
- `ERROR: ${diagnostic.code}: ${diagnostic.message}`,
12405
- `SUGGESTION: ${diagnostic.suggestion}`
12406
- ]
12474
+ stderr: [`ERROR: ${diagnostic.code}: ${diagnostic.message}`, `SUGGESTION: ${diagnostic.suggestion}`]
12407
12475
  };
12408
12476
  }
12409
12477
  execution = await executeUploadFlow({
@@ -13024,17 +13092,19 @@ function createAllureOpenHandler(deps = {}) {
13024
13092
  if (useJson) {
13025
13093
  return {
13026
13094
  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
- }))
13095
+ stdout: toJsonLine(
13096
+ buildJsonOutput({
13097
+ zipPath,
13098
+ dryRun: true,
13099
+ served: false,
13100
+ opened: false,
13101
+ keep,
13102
+ extractDir: extractDirRaw,
13103
+ rootDir: extractDirRaw,
13104
+ indexPath,
13105
+ summary
13106
+ })
13107
+ )
13038
13108
  };
13039
13109
  }
13040
13110
  return {
@@ -13084,20 +13154,24 @@ function createAllureOpenHandler(deps = {}) {
13084
13154
  }
13085
13155
  }
13086
13156
  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
- })));
13157
+ logger.log(
13158
+ JSON.stringify(
13159
+ buildJsonOutput({
13160
+ zipPath,
13161
+ dryRun: false,
13162
+ served: true,
13163
+ opened,
13164
+ keep,
13165
+ extractDir: extractResult.extractDir,
13166
+ rootDir: extractResult.rootDir,
13167
+ indexPath: extractResult.indexPath,
13168
+ url: serverHandle.url,
13169
+ port: serverHandle.port,
13170
+ host: serverHandle.host,
13171
+ summary
13172
+ })
13173
+ )
13174
+ );
13101
13175
  } else {
13102
13176
  logger.log(`Allure report extracted to ${extractResult.extractDir}.`);
13103
13177
  if (verbose) {
@@ -13385,13 +13459,15 @@ function createAllureHandler(deps = {}) {
13385
13459
  if (useJson) {
13386
13460
  return {
13387
13461
  exitCode: ExitCode.Success,
13388
- stdout: toJsonLine(buildDownloadJsonOutput({
13389
- issueKey: issueKey2,
13390
- dryRun: true,
13391
- downloaded: false,
13392
- attachment: selected,
13393
- outputPath
13394
- }))
13462
+ stdout: toJsonLine(
13463
+ buildDownloadJsonOutput({
13464
+ issueKey: issueKey2,
13465
+ dryRun: true,
13466
+ downloaded: false,
13467
+ attachment: selected,
13468
+ outputPath
13469
+ })
13470
+ )
13395
13471
  };
13396
13472
  }
13397
13473
  return {
@@ -13418,13 +13494,15 @@ function createAllureHandler(deps = {}) {
13418
13494
  if (useJson) {
13419
13495
  return {
13420
13496
  exitCode: ExitCode.Success,
13421
- stdout: toJsonLine(buildDownloadJsonOutput({
13422
- issueKey: issueKey2,
13423
- dryRun: false,
13424
- downloaded: true,
13425
- attachment: downloaded,
13426
- outputPath
13427
- }))
13497
+ stdout: toJsonLine(
13498
+ buildDownloadJsonOutput({
13499
+ issueKey: issueKey2,
13500
+ dryRun: false,
13501
+ downloaded: true,
13502
+ attachment: downloaded,
13503
+ outputPath
13504
+ })
13505
+ )
13428
13506
  };
13429
13507
  }
13430
13508
  return {
@@ -13472,14 +13550,16 @@ function createAllureHandler(deps = {}) {
13472
13550
  if (useJson) {
13473
13551
  return {
13474
13552
  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
- }))
13553
+ stdout: toJsonLine(
13554
+ buildJsonOutput2({
13555
+ issueKey,
13556
+ uploaded: false,
13557
+ commentAdded: false,
13558
+ dryRun: true,
13559
+ summary,
13560
+ attachment: { filename: attachmentFilename }
13561
+ })
13562
+ )
13483
13563
  };
13484
13564
  }
13485
13565
  return {
@@ -13515,14 +13595,16 @@ function createAllureHandler(deps = {}) {
13515
13595
  if (useJson) {
13516
13596
  return {
13517
13597
  exitCode: ExitCode.Success,
13518
- stdout: toJsonLine(buildJsonOutput2({
13519
- issueKey,
13520
- uploaded: true,
13521
- commentAdded,
13522
- dryRun: false,
13523
- summary,
13524
- attachment
13525
- }))
13598
+ stdout: toJsonLine(
13599
+ buildJsonOutput2({
13600
+ issueKey,
13601
+ uploaded: true,
13602
+ commentAdded,
13603
+ dryRun: false,
13604
+ summary,
13605
+ attachment
13606
+ })
13607
+ )
13526
13608
  };
13527
13609
  }
13528
13610
  return {
@@ -13860,9 +13942,11 @@ function parseScenarioFeatureFile(raw, filePath) {
13860
13942
  if (!featureName || !scenarioName || !scenarioKey || steps.length === 0 || scenarioCount !== 1) {
13861
13943
  return null;
13862
13944
  }
13863
- const linkedIssueKeys = [...new Set(
13864
- tags.map((tag) => tag.replace(/^@/, "").trim().toUpperCase()).filter((tag) => ISSUE_KEY_PATTERN.test(tag))
13865
- )];
13945
+ const linkedIssueKeys = [
13946
+ ...new Set(
13947
+ tags.map((tag) => tag.replace(/^@/, "").trim().toUpperCase()).filter((tag) => ISSUE_KEY_PATTERN.test(tag))
13948
+ )
13949
+ ];
13866
13950
  return {
13867
13951
  scenarioKey,
13868
13952
  featureName,
@@ -14060,7 +14144,9 @@ function createBddHandler(deps = {}) {
14060
14144
  stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
14061
14145
  };
14062
14146
  }
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));
14147
+ const features = (featureId ? result.data.filter((feature) => feature.id === featureId) : result.data).sort(
14148
+ (left, right) => left.payload.name.localeCompare(right.payload.name) || left.createdAt.localeCompare(right.createdAt)
14149
+ );
14064
14150
  if (featureId && features.length === 0) {
14065
14151
  return {
14066
14152
  exitCode: ExitCode.RemoteError,
@@ -14180,7 +14266,9 @@ function createBddHandler(deps = {}) {
14180
14266
  stderr: [`ERROR: ${scenariosResult.error.code}: ${scenariosResult.error.message}`]
14181
14267
  };
14182
14268
  }
14183
- const selectedScenarios = exportAll ? [...scenariosResult.data].sort((left, right) => (left.key ?? left.id).localeCompare(right.key ?? right.id)) : scenariosResult.data.filter(
14269
+ const selectedScenarios = exportAll ? [...scenariosResult.data].sort(
14270
+ (left, right) => (left.key ?? left.id).localeCompare(right.key ?? right.id)
14271
+ ) : scenariosResult.data.filter(
14184
14272
  (item) => item.id === scenarioSelector2 || (item.key ?? "").toUpperCase() === scenarioSelector2.toUpperCase()
14185
14273
  );
14186
14274
  if (!exportAll && selectedScenarios.length === 0) {
@@ -14326,7 +14414,9 @@ function createBddHandler(deps = {}) {
14326
14414
  if (!scenario) {
14327
14415
  return {
14328
14416
  exitCode: ExitCode.RemoteError,
14329
- stderr: [`ERROR: NOT_FOUND: BDD scenario ${parsedScenario.scenarioKey} was not found in this project.`]
14417
+ stderr: [
14418
+ `ERROR: NOT_FOUND: BDD scenario ${parsedScenario.scenarioKey} was not found in this project.`
14419
+ ]
14330
14420
  };
14331
14421
  }
14332
14422
  const nextTags = parsedScenario.tags.filter((tag) => !SCENARIO_METADATA_PATTERN.test(tag));
@@ -14432,7 +14522,9 @@ function createBddHandler(deps = {}) {
14432
14522
  if (!scenarioId && !testCaseKey) {
14433
14523
  return {
14434
14524
  exitCode: ExitCode.ValidationError,
14435
- stderr: ["ERROR: Provide either --id <SCENARIO_ID> or --test-case-key <TC-197> for testops bdd scenarios run."]
14525
+ stderr: [
14526
+ "ERROR: Provide either --id <SCENARIO_ID> or --test-case-key <TC-197> for testops bdd scenarios run."
14527
+ ]
14436
14528
  };
14437
14529
  }
14438
14530
  try {
@@ -14706,10 +14798,7 @@ function listToLines(items) {
14706
14798
  if (items.length === 0) {
14707
14799
  return ["Test cases: 0", "No test cases found for the selected context."];
14708
14800
  }
14709
- return [
14710
- `Test cases: ${items.length}`,
14711
- ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
14712
- ];
14801
+ return [`Test cases: ${items.length}`, ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)];
14713
14802
  }
14714
14803
  function showToLines(testCase) {
14715
14804
  return [
@@ -15009,11 +15098,7 @@ var CONFIG_FLAGS5 = /* @__PURE__ */ new Set([
15009
15098
  "--jira-email",
15010
15099
  "--jira-api-token"
15011
15100
  ]);
15012
- var DIRECT_JIRA_FLAGS = /* @__PURE__ */ new Set([
15013
- "--site",
15014
- "--email",
15015
- "--api-token"
15016
- ]);
15101
+ var DIRECT_JIRA_FLAGS = /* @__PURE__ */ new Set(["--site", "--email", "--api-token"]);
15017
15102
  function parseArgs6(args) {
15018
15103
  const flags = {};
15019
15104
  const boolFlags = /* @__PURE__ */ new Set();
@@ -15213,27 +15298,33 @@ function createDoctorHandler(deps = {}) {
15213
15298
  message: "Issue access check skipped. Pass --issue-key to verify Jira read/download access for a specific issue."
15214
15299
  });
15215
15300
  }
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
- ));
15301
+ checks.push(
15302
+ commandCheck(
15303
+ { execFileSync: runCommand },
15304
+ "unzip",
15305
+ ["-v"],
15306
+ "unzip is available for manual ZIP inspection.",
15307
+ "unzip was not found. The future built-in opener can avoid this, but manual inspection will be less convenient."
15308
+ )
15309
+ );
15310
+ checks.push(
15311
+ commandCheck(
15312
+ { execFileSync: runCommand },
15313
+ "java",
15314
+ ["-version"],
15315
+ "Java is available for optional raw allure-results workflows.",
15316
+ "Java was not found. This does not block generated Allure HTML ZIP upload/download/open; it only matters for raw allure-results generation."
15317
+ )
15318
+ );
15319
+ checks.push(
15320
+ commandCheck(
15321
+ { execFileSync: runCommand },
15322
+ "allure",
15323
+ ["--version"],
15324
+ "Allure CLI is available for optional raw allure-results workflows.",
15325
+ "Allure CLI was not found. This does not block generated Allure HTML ZIP upload/download/open."
15326
+ )
15327
+ );
15237
15328
  const summary2 = {
15238
15329
  status: computeOverallStatus(checks),
15239
15330
  checks
@@ -15272,9 +15363,7 @@ function createDoctorHandler(deps = {}) {
15272
15363
  message: "Forge endpoint is configured."
15273
15364
  });
15274
15365
  }
15275
- const hasContext = Boolean(
15276
- resolution.values.projectKey || resolution.values.issueKey
15277
- );
15366
+ const hasContext = Boolean(resolution.values.projectKey || resolution.values.issueKey);
15278
15367
  if (enforceContextCheck || hasContext) {
15279
15368
  if (!hasContext) {
15280
15369
  checks.push({
@@ -15501,11 +15590,7 @@ function createIngestFeatureHandler(deps = {}) {
15501
15590
  }
15502
15591
  return {
15503
15592
  exitCode: ExitCode.RemoteError,
15504
- stdout: [
15505
- "Feature ingestion: FAILED",
15506
- `Feature: ${name}`,
15507
- `Source: ${useStdin ? "stdin" : sourceFile}`
15508
- ],
15593
+ stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
15509
15594
  stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
15510
15595
  };
15511
15596
  }
@@ -15546,11 +15631,7 @@ function createIngestFeatureHandler(deps = {}) {
15546
15631
  }
15547
15632
  return {
15548
15633
  exitCode: ExitCode.TransportError,
15549
- stdout: [
15550
- "Feature ingestion: FAILED",
15551
- `Feature: ${name}`,
15552
- `Source: ${useStdin ? "stdin" : sourceFile}`
15553
- ],
15634
+ stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
15554
15635
  stderr: [`ERROR: ${normalizeError5(error)}`]
15555
15636
  };
15556
15637
  }
@@ -16145,14 +16226,8 @@ function buildGitHubSetupPlan(input) {
16145
16226
  const workflowId = requireNonEmpty(input.workflowId, "workflow id");
16146
16227
  const workflowPath = normalizeWorkflowPath(input.workflowPath);
16147
16228
  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
- );
16229
+ const endpointSecretName = normalizeSecretName(input.callbackEndpointSecretName, "callback endpoint secret name");
16230
+ const tokenSecretName = normalizeSecretName(input.callbackAuthTokenSecretName, "callback auth token secret name");
16156
16231
  if (endpointSecretName === tokenSecretName) {
16157
16232
  throw new Error("callback endpoint and auth token secret names must be different.");
16158
16233
  }
@@ -16211,13 +16286,7 @@ function buildGitHubSetupPlan(input) {
16211
16286
  "Secrets: read/write metadata only for the explicit github-secrets apply scope.",
16212
16287
  "Contents: read for doctor; this command never creates commits or pull requests."
16213
16288
  ],
16214
- remoteChangesExcluded: [
16215
- "accounts",
16216
- "credentials",
16217
- "commits",
16218
- "pull requests",
16219
- "workflow runs"
16220
- ]
16289
+ remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "workflow runs"]
16221
16290
  }
16222
16291
  },
16223
16292
  jiraProfile: {
@@ -16435,15 +16504,7 @@ function validateSetupPlan(value) {
16435
16504
  errors.push("project.key is required.");
16436
16505
  }
16437
16506
  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
- ])) {
16507
+ if (!isRecord(github) || !hasExactKeys(github, ["owner", "repository", "ref", "workflowId", "workflowFile", "requiredSecrets", "guidance"])) {
16447
16508
  errors.push("github has an invalid shape.");
16448
16509
  } else {
16449
16510
  for (const field of ["owner", "repository", "ref", "workflowId"]) {
@@ -16520,6 +16581,8 @@ function hashSetupContent(value) {
16520
16581
  }
16521
16582
 
16522
16583
  // src/githubSetupProvider.ts
16584
+ var GITHUB_SECRET_PAGE_SIZE = 100;
16585
+ var MAX_GITHUB_SECRET_PAGES = 100;
16523
16586
  function isRecord2(value) {
16524
16587
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16525
16588
  }
@@ -16551,18 +16614,14 @@ async function githubApi(fetchImpl, url, token) {
16551
16614
  }
16552
16615
  async function setGitHubRepositorySecretWithCli(input) {
16553
16616
  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
- }
16617
+ const child = (0, import_node_child_process4.spawn)("gh", ["secret", "set", input.name, "--repo", `${input.owner}/${input.repository}`], {
16618
+ shell: false,
16619
+ stdio: ["pipe", "ignore", "pipe"],
16620
+ env: {
16621
+ ...process.env,
16622
+ GH_TOKEN: input.adminToken
16564
16623
  }
16565
- );
16624
+ });
16566
16625
  let stderr = "";
16567
16626
  child.stderr.setEncoding("utf8");
16568
16627
  child.stderr.on("data", (chunk) => {
@@ -16622,6 +16681,7 @@ var GitHubSetupProvider = class {
16622
16681
  return {
16623
16682
  found: true,
16624
16683
  active: state === "active",
16684
+ state,
16625
16685
  path: workflowPath,
16626
16686
  contentSha256,
16627
16687
  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 +16696,52 @@ var GitHubSetupProvider = class {
16636
16696
  };
16637
16697
  }
16638
16698
  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
- }
16699
+ const baseUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/secrets`;
16646
16700
  const names = /* @__PURE__ */ new Set();
16647
- if (isRecord2(response.body) && Array.isArray(response.body.secrets)) {
16648
- for (const item of response.body.secrets) {
16701
+ let totalCount;
16702
+ let pagesRead = 0;
16703
+ for (let page = 1; page <= MAX_GITHUB_SECRET_PAGES; page += 1) {
16704
+ const url = `${baseUrl}?per_page=${GITHUB_SECRET_PAGE_SIZE}&page=${page}`;
16705
+ const response = await githubApi(this.fetchImpl, url, adminToken);
16706
+ if (!response.ok) {
16707
+ throw new Error(
16708
+ `GitHub repository secret metadata request failed on page ${page} with HTTP ${response.status}: ${safeGitHubMessage(response.body, "request failed")}`
16709
+ );
16710
+ }
16711
+ const body = isRecord2(response.body) ? response.body : {};
16712
+ if (totalCount === void 0 && typeof body.total_count === "number" && Number.isFinite(body.total_count)) {
16713
+ totalCount = Math.max(0, Math.trunc(body.total_count));
16714
+ }
16715
+ const secrets = Array.isArray(body.secrets) ? body.secrets : [];
16716
+ const before = names.size;
16717
+ for (const item of secrets) {
16649
16718
  if (isRecord2(item) && typeof item.name === "string") {
16650
16719
  names.add(item.name);
16651
16720
  }
16652
16721
  }
16722
+ pagesRead = page;
16723
+ if (totalCount !== void 0 && names.size >= totalCount) {
16724
+ break;
16725
+ }
16726
+ if (secrets.length < GITHUB_SECRET_PAGE_SIZE) {
16727
+ if (totalCount !== void 0 && names.size < totalCount) {
16728
+ throw new Error(
16729
+ `GitHub repository secret metadata pagination ended early after ${names.size} of ${totalCount} secret name(s).`
16730
+ );
16731
+ }
16732
+ break;
16733
+ }
16734
+ if (names.size === before) {
16735
+ throw new Error("GitHub repository secret metadata pagination made no progress.");
16736
+ }
16737
+ if (page === MAX_GITHUB_SECRET_PAGES) {
16738
+ throw new Error(`GitHub repository secret metadata exceeded ${MAX_GITHUB_SECRET_PAGES} pages.`);
16739
+ }
16653
16740
  }
16654
16741
  return {
16655
16742
  verified: true,
16656
16743
  names,
16657
- message: `Verified ${names.size} GitHub repository secret name(s); values remain unreadable.`
16744
+ message: `Verified ${names.size} GitHub repository secret name(s) across ${pagesRead} page(s); values remain unreadable.`
16658
16745
  };
16659
16746
  }
16660
16747
  async setCallbackSecret(plan, secret, value, adminToken) {
@@ -16668,18 +16755,295 @@ var GitHubSetupProvider = class {
16668
16755
  }
16669
16756
  };
16670
16757
 
16758
+ // src/azureDevOpsSetupProvider.ts
16759
+ var isRecord3 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
16760
+ async function adoGet(fetchImpl, url, token) {
16761
+ const response = await fetchImpl(url, {
16762
+ headers: {
16763
+ accept: "application/json",
16764
+ ...token ? { authorization: `Basic ${Buffer.from(`:${token}`).toString("base64")}` } : {}
16765
+ }
16766
+ });
16767
+ const text = await response.text();
16768
+ let body = {};
16769
+ try {
16770
+ body = text ? JSON.parse(text) : {};
16771
+ } catch {
16772
+ }
16773
+ return { ok: response.ok, status: response.status, body };
16774
+ }
16775
+ async function setAzurePipelineVariableWithRest(input, fetchImpl) {
16776
+ const base = `https://dev.azure.com/${encodeURIComponent(input.organization)}/${encodeURIComponent(input.project)}/_apis/build/definitions/${encodeURIComponent(input.pipelineId)}`;
16777
+ const auth = { authorization: `Basic ${Buffer.from(`:${input.token}`).toString("base64")}` };
16778
+ const current = await adoGet(fetchImpl, `${base}?api-version=7.1`, input.token);
16779
+ if (!current.ok || !isRecord3(current.body))
16780
+ throw new Error(`Azure DevOps build definition could not be read (HTTP ${current.status}).`);
16781
+ const variables = isRecord3(current.body.variables) ? { ...current.body.variables } : {};
16782
+ variables[input.name] = { value: input.value, isSecret: true };
16783
+ const query = new URLSearchParams({
16784
+ "api-version": "7.1",
16785
+ secretsSourceDefinitionId: String(current.body.id),
16786
+ secretsSourceDefinitionRevision: String(current.body.revision)
16787
+ });
16788
+ const response = await fetchImpl(`${base}?${query.toString()}`, {
16789
+ method: "PUT",
16790
+ headers: { ...auth, "content-type": "application/json", accept: "application/json" },
16791
+ body: JSON.stringify({ ...current.body, variables })
16792
+ });
16793
+ if (!response.ok) throw new Error(`Azure DevOps pipeline variable update failed (HTTP ${response.status}).`);
16794
+ }
16795
+ function azurePipelineStateMessage(name, queueStatus) {
16796
+ switch (queueStatus) {
16797
+ case "enabled":
16798
+ return `Azure DevOps pipeline ${name} is reachable and enabled. Inspection never queues a run.`;
16799
+ case "paused":
16800
+ return `Azure DevOps pipeline ${name} is reachable but paused; builds may be queued but will not start until the definition is enabled.`;
16801
+ case "disabled":
16802
+ return `Azure DevOps pipeline ${name} is reachable but disabled; new builds cannot be queued until the definition is enabled.`;
16803
+ default:
16804
+ return `Azure DevOps pipeline ${name} is reachable, but its queue status is ${queueStatus ?? "unknown"}.`;
16805
+ }
16806
+ }
16807
+ var AzureDevOpsSetupProvider = class {
16808
+ name = "azure-devops";
16809
+ fetchImpl;
16810
+ setter;
16811
+ constructor(deps = {}) {
16812
+ this.fetchImpl = deps.fetchImpl ?? fetch;
16813
+ this.setter = deps.setPipelineVariable ?? ((input) => setAzurePipelineVariableWithRest(input, this.fetchImpl));
16814
+ }
16815
+ async inspectExecutionDefinition(plan, token) {
16816
+ const a = plan.azureDevOps;
16817
+ const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
16818
+ const result = await adoGet(this.fetchImpl, url, token);
16819
+ if (!result.ok)
16820
+ return {
16821
+ found: false,
16822
+ message: result.status === 404 ? "Azure DevOps pipeline was not found." : `Azure DevOps pipeline metadata could not be verified (HTTP ${result.status}).`
16823
+ };
16824
+ const body = isRecord3(result.body) ? result.body : {};
16825
+ const name = typeof body.name === "string" ? body.name : a.pipelineId;
16826
+ const queueStatus = typeof body.queueStatus === "string" ? body.queueStatus : void 0;
16827
+ return {
16828
+ found: true,
16829
+ active: queueStatus === "enabled",
16830
+ state: queueStatus,
16831
+ path: name,
16832
+ message: azurePipelineStateMessage(name, queueStatus)
16833
+ };
16834
+ }
16835
+ async inspectCallbackSecretMetadata(plan, token) {
16836
+ if (!token)
16837
+ return {
16838
+ verified: false,
16839
+ names: /* @__PURE__ */ new Set(),
16840
+ message: "Azure DevOps pipeline variable metadata was not verified because azureDevOpsAdminToken was not provided. Secret values are never readable."
16841
+ };
16842
+ const a = plan.azureDevOps;
16843
+ const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
16844
+ const result = await adoGet(this.fetchImpl, url, token);
16845
+ if (!result.ok) throw new Error(`Azure DevOps pipeline metadata request failed with HTTP ${result.status}.`);
16846
+ const variables = isRecord3(result.body) && isRecord3(result.body.variables) ? result.body.variables : {};
16847
+ return {
16848
+ verified: true,
16849
+ names: new Set(Object.keys(variables)),
16850
+ message: `Verified ${Object.keys(variables).length} Azure DevOps pipeline variable name(s); secret values remain unreadable.`
16851
+ };
16852
+ }
16853
+ async setCallbackSecret(plan, secret, value, adminToken) {
16854
+ if (!this.setter)
16855
+ throw new Error("Azure DevOps pipeline variable setter is not configured; use the safe Azure CLI adapter.");
16856
+ await this.setter({
16857
+ organization: plan.azureDevOps.organization,
16858
+ project: plan.azureDevOps.project,
16859
+ pipelineId: plan.azureDevOps.pipelineId,
16860
+ name: secret.repositorySecretName,
16861
+ value,
16862
+ token: adminToken
16863
+ });
16864
+ }
16865
+ };
16866
+
16867
+ // src/azureDevOpsSetupPlan.ts
16868
+ var import_node_crypto2 = require("node:crypto");
16869
+ var digest = (v) => `sha256:${(0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(v)).digest("hex")}`;
16870
+ var required = (value, field) => {
16871
+ const result = value.trim();
16872
+ if (!result) throw new Error(`${field} is required.`);
16873
+ return result;
16874
+ };
16875
+ var variableName = (value, field) => {
16876
+ const result = required(value, field).toUpperCase();
16877
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(result)) throw new Error(`${field} is invalid.`);
16878
+ return result;
16879
+ };
16880
+ function buildAzureDevOpsSetupPlan(input) {
16881
+ const projectKey = required(input.projectKey, "project key").toUpperCase();
16882
+ if (!/^[A-Z][A-Z0-9_]{1,31}$/.test(projectKey)) throw new Error("project key is invalid.");
16883
+ const organization = required(input.organization, "organization");
16884
+ const azureProject = required(input.azureProject, "Azure project");
16885
+ const pipelineId = required(input.pipelineId, "pipeline id");
16886
+ if (!/^\d+$/.test(pipelineId)) throw new Error("pipeline id must be numeric.");
16887
+ const ref = required(input.ref, "ref");
16888
+ const apiVersion = required(input.apiVersion ?? "7.1", "api version");
16889
+ const profileLabel = required(input.profileLabel, "profile label");
16890
+ const endpointName = variableName(input.callbackEndpointVariableName, "callback endpoint variable name");
16891
+ const tokenName = variableName(input.callbackAuthTokenVariableName, "callback auth token variable name");
16892
+ if (endpointName === tokenName) throw new Error("callback variable names must be distinct.");
16893
+ const requiredSecrets = [
16894
+ {
16895
+ repositorySecretName: endpointName,
16896
+ valueKey: "callbackEndpoint",
16897
+ purpose: "Forge callback endpoint pipeline variable."
16898
+ },
16899
+ {
16900
+ repositorySecretName: tokenName,
16901
+ valueKey: "callbackAuthToken",
16902
+ purpose: "Forge callback auth token pipeline variable."
16903
+ }
16904
+ ];
16905
+ const scopes = [
16906
+ "provider-secrets",
16907
+ "jira-profile",
16908
+ ...input.setProjectDefault ? ["project-default"] : []
16909
+ ];
16910
+ const actions = scopes.map((scope) => ({
16911
+ id: scope === "provider-secrets" ? "set-provider-secrets" : scope === "jira-profile" ? "upsert-jira-profile" : "set-project-default",
16912
+ scope,
16913
+ summary: `Apply ${scope} for Azure DevOps.`,
16914
+ mutates: "jira"
16915
+ }));
16916
+ const withoutId = {
16917
+ schemaVersion: "automatify.testops.setup/v1",
16918
+ kind: "AutomatifyTestOpsSetupPlan",
16919
+ provider: "azure-devops",
16920
+ project: { key: projectKey },
16921
+ azureDevOps: {
16922
+ organization,
16923
+ project: azureProject,
16924
+ pipelineId,
16925
+ ref,
16926
+ apiVersion,
16927
+ requiredSecrets,
16928
+ guidance: {
16929
+ authentication: "Use an Azure DevOps PAT with least-privilege pipeline read/manage-variable access.",
16930
+ permissions: ["Pipelines: read for doctor; manage variables only for explicit provider-secrets apply."],
16931
+ remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "pipeline runs"]
16932
+ }
16933
+ },
16934
+ jiraProfile: {
16935
+ label: profileLabel,
16936
+ enabled: input.enabled,
16937
+ provider: "azureDevops",
16938
+ config: {
16939
+ organization,
16940
+ project: azureProject,
16941
+ pipelineId,
16942
+ apiVersion,
16943
+ bodyTemplate: JSON.stringify({ resources: { repositories: { self: { refName: ref } } } })
16944
+ },
16945
+ setProjectDefault: input.setProjectDefault
16946
+ },
16947
+ actions,
16948
+ smokeValidation: [
16949
+ {
16950
+ id: "pipeline-content",
16951
+ verifies: "Azure DevOps pipeline metadata without queueing a run.",
16952
+ triggersExternalRun: false
16953
+ },
16954
+ {
16955
+ id: "pipeline-variable-metadata",
16956
+ verifies: "Required variable names; values remain unreadable.",
16957
+ triggersExternalRun: false
16958
+ },
16959
+ {
16960
+ id: "jira-profile",
16961
+ verifies: "Forge automation profile and optional default.",
16962
+ triggersExternalRun: false
16963
+ }
16964
+ ],
16965
+ rollback: actions.map((a) => ({
16966
+ actionId: a.id,
16967
+ strategy: "Restore the previous state manually; secret values cannot be read back.",
16968
+ automatic: false
16969
+ })),
16970
+ followUp: {
16971
+ azureDevOps: "Azure DevOps setup is implemented through this provider-neutral plan/apply/doctor boundary."
16972
+ }
16973
+ };
16974
+ return { ...withoutId, planId: digest(withoutId) };
16975
+ }
16976
+ function validateAzureDevOpsSetupPlan(value) {
16977
+ const errors = [];
16978
+ const record = value;
16979
+ const exact = (v, keys) => Boolean(
16980
+ v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).sort().join() === [...keys].sort().join()
16981
+ );
16982
+ if (!exact(value, [
16983
+ "schemaVersion",
16984
+ "kind",
16985
+ "planId",
16986
+ "provider",
16987
+ "project",
16988
+ "azureDevOps",
16989
+ "jiraProfile",
16990
+ "actions",
16991
+ "smokeValidation",
16992
+ "rollback",
16993
+ "followUp"
16994
+ ]))
16995
+ errors.push("Plan has missing or unsupported top-level fields.");
16996
+ if (record?.schemaVersion !== "automatify.testops.setup/v1" || record?.kind !== "AutomatifyTestOpsSetupPlan" || record?.provider !== "azure-devops")
16997
+ errors.push("Plan schema, kind, or provider is invalid.");
16998
+ if (!exact(record?.project, ["key"]) || !/^[A-Z][A-Z0-9_]{1,31}$/.test(record?.project?.key ?? ""))
16999
+ errors.push("project.key is invalid.");
17000
+ const a = record?.azureDevOps;
17001
+ 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 ?? ""))
17002
+ errors.push("azureDevOps has an invalid shape or fields.");
17003
+ 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(
17004
+ (s) => exact(s, ["repositorySecretName", "valueKey", "purpose"]) && /^[A-Z_][A-Z0-9_]*$/.test(s.repositorySecretName) && s.purpose
17005
+ ))
17006
+ errors.push("azureDevOps.requiredSecrets is invalid.");
17007
+ const p = record?.jiraProfile;
17008
+ 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)
17009
+ errors.push("jiraProfile/config is invalid or inconsistent.");
17010
+ const ids = p?.setProjectDefault ? ["set-provider-secrets", "upsert-jira-profile", "set-project-default"] : ["set-provider-secrets", "upsert-jira-profile"];
17011
+ const scopes = p?.setProjectDefault ? ["provider-secrets", "jira-profile", "project-default"] : ["provider-secrets", "jira-profile"];
17012
+ if (!Array.isArray(record?.actions) || record.actions.map((x) => x.id).join() !== ids.join() || !record.actions.every(
17013
+ (x, i) => exact(x, ["id", "scope", "summary", "mutates"]) && x.scope === scopes[i] && x.mutates === "jira"
17014
+ ))
17015
+ errors.push("actions are invalid.");
17016
+ const smokeIds = ["pipeline-content", "pipeline-variable-metadata", "jira-profile"];
17017
+ if (!Array.isArray(record?.smokeValidation) || record.smokeValidation.length !== 3 || !record.smokeValidation.every(
17018
+ (x, i) => exact(x, ["id", "verifies", "triggersExternalRun"]) && x.id === smokeIds[i] && x.triggersExternalRun === false
17019
+ ))
17020
+ errors.push("smokeValidation is invalid.");
17021
+ if (!Array.isArray(record?.rollback) || record.rollback.length !== ids.length || !record.rollback.every(
17022
+ (x, i) => exact(x, ["actionId", "strategy", "automatic"]) && x.actionId === ids[i] && x.automatic === false
17023
+ ))
17024
+ errors.push("rollback is invalid.");
17025
+ if (errors.length === 0) {
17026
+ const { planId, ...rest } = record;
17027
+ if (planId !== digest(rest)) errors.push("planId does not match the plan contents.");
17028
+ }
17029
+ return errors.length ? { ok: false, errors } : { ok: true, errors, plan: value };
17030
+ }
17031
+
16671
17032
  // src/setup.ts
16672
17033
  var APPLY_RESULT_SCHEMA_VERSION = "automatify.testops.setup.apply/v1";
16673
17034
  var DOCTOR_RESULT_SCHEMA_VERSION = "automatify.testops.setup.doctor/v1";
16674
17035
  var SECRET_KEYS = [
16675
17036
  "githubProviderToken",
16676
17037
  "githubAdminToken",
17038
+ "azureDevOpsProviderToken",
17039
+ "azureDevOpsAdminToken",
16677
17040
  "callbackEndpoint",
16678
17041
  "callbackAuthToken"
16679
17042
  ];
16680
17043
  var MUTATION_SCOPES = [
16681
17044
  "workflow-file",
16682
17045
  "github-secrets",
17046
+ "provider-secrets",
16683
17047
  "jira-profile",
16684
17048
  "project-default"
16685
17049
  ];
@@ -16704,7 +17068,13 @@ var PLAN_VALUE_FLAGS = /* @__PURE__ */ new Set([
16704
17068
  "--workflow-source",
16705
17069
  "--profile-label",
16706
17070
  "--callback-endpoint-secret-name",
16707
- "--callback-auth-token-secret-name"
17071
+ "--callback-auth-token-secret-name",
17072
+ "--organization",
17073
+ "--azure-project",
17074
+ "--pipeline-id",
17075
+ "--api-version",
17076
+ "--callback-endpoint-variable-name",
17077
+ "--callback-auth-token-variable-name"
16708
17078
  ]);
16709
17079
  var APPLY_VALUE_FLAGS = /* @__PURE__ */ new Set(["--plan", "--repo-root"]);
16710
17080
  var APPLY_BOOL_FLAGS = /* @__PURE__ */ new Set([
@@ -16815,7 +17185,7 @@ function loadPlan(planPath, cwd) {
16815
17185
  ExitCode.ValidationError
16816
17186
  );
16817
17187
  }
16818
- const validation = validateSetupPlan(parsed);
17188
+ const validation = isAzurePlanValue(parsed) ? validateAzureDevOpsSetupPlan(parsed) : validateSetupPlan(parsed);
16819
17189
  if (!validation.ok || !validation.plan) {
16820
17190
  throw new SetupCommandError(
16821
17191
  "PLAN_VALIDATION_ERROR",
@@ -16825,6 +17195,9 @@ function loadPlan(planPath, cwd) {
16825
17195
  }
16826
17196
  return validation.plan;
16827
17197
  }
17198
+ function isAzurePlanValue(value) {
17199
+ return Boolean(value && typeof value === "object" && value.provider === "azure-devops");
17200
+ }
16828
17201
  function isSecretKey(value) {
16829
17202
  return SECRET_KEYS.includes(value);
16830
17203
  }
@@ -16844,14 +17217,26 @@ async function resolveSecrets(parsed, deps) {
16844
17217
  );
16845
17218
  }
16846
17219
  if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
16847
- throw new SetupCommandError("SECRET_INPUT_ERROR", "Secret stdin must be a JSON object.", ExitCode.ValidationError);
17220
+ throw new SetupCommandError(
17221
+ "SECRET_INPUT_ERROR",
17222
+ "Secret stdin must be a JSON object.",
17223
+ ExitCode.ValidationError
17224
+ );
16848
17225
  }
16849
17226
  for (const [key, value] of Object.entries(envelope)) {
16850
17227
  if (!isSecretKey(key)) {
16851
- throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin contains unsupported key ${key}.`, ExitCode.ValidationError);
17228
+ throw new SetupCommandError(
17229
+ "SECRET_INPUT_ERROR",
17230
+ `Secret stdin contains unsupported key ${key}.`,
17231
+ ExitCode.ValidationError
17232
+ );
16852
17233
  }
16853
17234
  if (typeof value !== "string" || !value.trim()) {
16854
- throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin key ${key} must be a non-empty string.`, ExitCode.ValidationError);
17235
+ throw new SetupCommandError(
17236
+ "SECRET_INPUT_ERROR",
17237
+ `Secret stdin key ${key} must be a non-empty string.`,
17238
+ ExitCode.ValidationError
17239
+ );
16855
17240
  }
16856
17241
  values[key] = value.trim();
16857
17242
  sources[key] = "stdin";
@@ -16870,10 +17255,18 @@ async function resolveSecrets(parsed, deps) {
16870
17255
  }
16871
17256
  const envName = target.slice(4);
16872
17257
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) {
16873
- throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret reference for ${key} has an invalid environment variable name.`, ExitCode.ValidationError);
17258
+ throw new SetupCommandError(
17259
+ "SECRET_REFERENCE_ERROR",
17260
+ `Secret reference for ${key} has an invalid environment variable name.`,
17261
+ ExitCode.ValidationError
17262
+ );
16874
17263
  }
16875
17264
  if (sources[key]) {
16876
- throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret ${key} was provided more than once.`, ExitCode.ValidationError);
17265
+ throw new SetupCommandError(
17266
+ "SECRET_REFERENCE_ERROR",
17267
+ `Secret ${key} was provided more than once.`,
17268
+ ExitCode.ValidationError
17269
+ );
16877
17270
  }
16878
17271
  const value = deps.env[envName]?.trim();
16879
17272
  if (!value) {
@@ -16908,7 +17301,11 @@ function approvedScopes(plan, parsed, dryRun) {
16908
17301
  throw new SetupCommandError("APPROVAL_ERROR", `Unsupported approval scope ${scope}.`, ExitCode.UsageError);
16909
17302
  }
16910
17303
  if (!planScopes.includes(scope)) {
16911
- throw new SetupCommandError("APPROVAL_ERROR", `Approval scope ${scope} is not present in this plan.`, ExitCode.UsageError);
17304
+ throw new SetupCommandError(
17305
+ "APPROVAL_ERROR",
17306
+ `Approval scope ${scope} is not present in this plan.`,
17307
+ ExitCode.UsageError
17308
+ );
16912
17309
  }
16913
17310
  expanded.add(scope);
16914
17311
  }
@@ -16981,10 +17378,7 @@ async function prepareState(plan, scopes, secrets, context, deps, dryRun, rotate
16981
17378
  );
16982
17379
  }
16983
17380
  if (secrets.values.githubAdminToken) {
16984
- const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(
16985
- plan,
16986
- secrets.values.githubAdminToken
16987
- );
17381
+ const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(plan, secrets.values.githubAdminToken);
16988
17382
  repositorySecretNames = inspection.names;
16989
17383
  const secretsToWrite = plan.github.requiredSecrets.filter(
16990
17384
  (secret) => rotateSecrets || !repositorySecretNames?.has(secret.repositorySecretName)
@@ -17087,12 +17481,7 @@ async function applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecr
17087
17481
  }
17088
17482
  const adminToken = secrets.values.githubAdminToken;
17089
17483
  for (const secret of pending) {
17090
- await deps.githubProvider.setCallbackSecret(
17091
- plan,
17092
- secret,
17093
- secrets.values[secret.valueKey],
17094
- adminToken
17095
- );
17484
+ await deps.githubProvider.setCallbackSecret(plan, secret, secrets.values[secret.valueKey], adminToken);
17096
17485
  }
17097
17486
  return {
17098
17487
  id: action.id,
@@ -17318,11 +17707,39 @@ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
17318
17707
  function planCommand(args, deps) {
17319
17708
  const parsed = parseArgs10(args, PLAN_VALUE_FLAGS, /* @__PURE__ */ new Set(["--set-default", "--disabled"]));
17320
17709
  if (parsed.errors.length > 0) {
17321
- return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors), ExitCode.UsageError);
17710
+ return jsonResponse(
17711
+ errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors),
17712
+ ExitCode.UsageError
17713
+ );
17322
17714
  }
17323
- if ((parsed.flags["--provider"] ?? "github-actions") !== "github-actions") {
17715
+ const provider = parsed.flags["--provider"] ?? "github-actions";
17716
+ if (provider === "azure-devops") {
17717
+ try {
17718
+ return jsonResponse(
17719
+ buildAzureDevOpsSetupPlan({
17720
+ projectKey: parsed.flags["--project-key"] ?? "",
17721
+ organization: parsed.flags["--organization"] ?? "",
17722
+ azureProject: parsed.flags["--azure-project"] ?? "",
17723
+ pipelineId: parsed.flags["--pipeline-id"] ?? "",
17724
+ ref: parsed.flags["--ref"] ?? "main",
17725
+ apiVersion: parsed.flags["--api-version"],
17726
+ profileLabel: parsed.flags["--profile-label"] ?? "Azure DevOps",
17727
+ enabled: !parsed.boolFlags.has("--disabled"),
17728
+ setProjectDefault: parsed.boolFlags.has("--set-default"),
17729
+ callbackEndpointVariableName: parsed.flags["--callback-endpoint-variable-name"] ?? "TESTOPS_FORGE_ENDPOINT",
17730
+ callbackAuthTokenVariableName: parsed.flags["--callback-auth-token-variable-name"] ?? "TESTOPS_FORGE_AUTH_TOKEN"
17731
+ })
17732
+ );
17733
+ } catch (error) {
17734
+ return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
17735
+ }
17736
+ }
17737
+ if (provider !== "github-actions") {
17324
17738
  return jsonResponse(
17325
- errorPayload("VALIDATION_ERROR", "Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."),
17739
+ errorPayload(
17740
+ "VALIDATION_ERROR",
17741
+ "Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."
17742
+ ),
17326
17743
  ExitCode.ValidationError
17327
17744
  );
17328
17745
  }
@@ -17343,16 +17760,16 @@ function planCommand(args, deps) {
17343
17760
  });
17344
17761
  return jsonResponse(plan);
17345
17762
  } catch (error) {
17346
- return jsonResponse(
17347
- errorPayload("VALIDATION_ERROR", safeErrorMessage(error)),
17348
- ExitCode.ValidationError
17349
- );
17763
+ return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
17350
17764
  }
17351
17765
  }
17352
17766
  async function applyCommand(args, context, deps) {
17353
17767
  const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, APPLY_BOOL_FLAGS, true);
17354
17768
  if (parsed.errors.length > 0) {
17355
- return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors), ExitCode.UsageError);
17769
+ return jsonResponse(
17770
+ errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors),
17771
+ ExitCode.UsageError
17772
+ );
17356
17773
  }
17357
17774
  const planPath = parsed.flags["--plan"];
17358
17775
  if (!planPath) {
@@ -17369,21 +17786,15 @@ async function applyCommand(args, context, deps) {
17369
17786
  const actionResults = [];
17370
17787
  try {
17371
17788
  const plan = loadPlan(planPath, deps.cwd);
17372
- const scopes = approvedScopes(plan, parsed, dryRun);
17373
17789
  secrets = await resolveSecrets(parsed, deps);
17790
+ if (plan.provider === "azure-devops") {
17791
+ return await applyAzureCommand(plan, parsed, context, deps, secrets);
17792
+ }
17793
+ const scopes = approvedScopes(plan, parsed, dryRun);
17374
17794
  const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
17375
17795
  const rotateSecrets = parsed.boolFlags.has("--rotate-secrets");
17376
17796
  const rotateProviderToken = parsed.boolFlags.has("--rotate-provider-token");
17377
- const state = await prepareState(
17378
- plan,
17379
- scopes,
17380
- secrets,
17381
- context,
17382
- deps,
17383
- dryRun,
17384
- rotateSecrets,
17385
- rotateProviderToken
17386
- );
17797
+ const state = await prepareState(plan, scopes, secrets, context, deps, dryRun, rotateSecrets, rotateProviderToken);
17387
17798
  let appliedProfile = state.matchingProfile;
17388
17799
  for (const action of plan.actions) {
17389
17800
  if (!scopes.includes(action.scope)) {
@@ -17402,14 +17813,7 @@ async function applyCommand(args, context, deps) {
17402
17813
  } else if (action.scope === "github-secrets") {
17403
17814
  actionResults.push(await applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets));
17404
17815
  } else if (action.scope === "jira-profile") {
17405
- const applied = await applyJiraProfile(
17406
- plan,
17407
- state,
17408
- secrets,
17409
- context,
17410
- dryRun,
17411
- rotateProviderToken
17412
- );
17816
+ const applied = await applyJiraProfile(plan, state, secrets, context, dryRun, rotateProviderToken);
17413
17817
  appliedProfile = applied.profile;
17414
17818
  actionResults.push(applied.result);
17415
17819
  } else if (action.scope === "project-default") {
@@ -17469,18 +17873,149 @@ async function applyCommand(args, context, deps) {
17469
17873
  );
17470
17874
  }
17471
17875
  }
17472
- async function doctorCommand(args, context, deps) {
17473
- const parsed = parseArgs10(
17474
- args,
17475
- APPLY_VALUE_FLAGS,
17476
- /* @__PURE__ */ new Set(["--secrets-stdin"]),
17477
- true
17876
+ async function applyAzureCommand(plan, parsed, context, deps, secrets) {
17877
+ const dryRun = parsed.boolFlags.has("--dry-run");
17878
+ const scopes = approvedScopes(plan, parsed, dryRun);
17879
+ const profiles = requireServiceData(
17880
+ await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }),
17881
+ "listScenarioAutomationProfiles"
17478
17882
  );
17883
+ const current = profiles.find((item) => item.label === plan.jiraProfile.label);
17884
+ if (!dryRun && scopes.includes("provider-secrets") && !secrets.values.azureDevOpsAdminToken)
17885
+ throw new SetupCommandError(
17886
+ "SECRET_INPUT_REQUIRED",
17887
+ "azureDevOpsAdminToken is required for the approved Azure DevOps pipeline-variable scope.",
17888
+ ExitCode.ValidationError
17889
+ );
17890
+ const results = [];
17891
+ const secretNames = scopes.includes("provider-secrets") && secrets.values.azureDevOpsAdminToken ? (await deps.azureProvider.inspectCallbackSecretMetadata(plan, secrets.values.azureDevOpsAdminToken)).names : void 0;
17892
+ let appliedProfile = current;
17893
+ for (const action of plan.actions) {
17894
+ if (!scopes.includes(action.scope)) {
17895
+ results.push({
17896
+ id: action.id,
17897
+ scope: action.scope,
17898
+ status: "skipped",
17899
+ message: "Action was not in the explicit approval scope.",
17900
+ rollback: { available: false, guidance: "No mutation occurred." }
17901
+ });
17902
+ continue;
17903
+ }
17904
+ if (action.scope === "provider-secrets") {
17905
+ const pending = plan.azureDevOps.requiredSecrets.filter(
17906
+ (item) => parsed.boolFlags.has("--rotate-secrets") || !secretNames?.has(item.repositorySecretName)
17907
+ );
17908
+ if (!dryRun && pending.some((item) => !secrets.values[item.valueKey]))
17909
+ throw new SetupCommandError(
17910
+ "SECRET_INPUT_REQUIRED",
17911
+ "Azure DevOps callback secrets are required for approved pipeline variable changes.",
17912
+ ExitCode.ValidationError
17913
+ );
17914
+ if (!dryRun)
17915
+ for (const item of pending)
17916
+ await deps.azureProvider.setCallbackSecret(
17917
+ plan,
17918
+ item,
17919
+ secrets.values[item.valueKey],
17920
+ secrets.values.azureDevOpsAdminToken
17921
+ );
17922
+ results.push({
17923
+ id: action.id,
17924
+ scope: action.scope,
17925
+ status: dryRun ? "planned" : pending.length ? "updated" : "skipped",
17926
+ message: "Azure DevOps pipeline variable names are managed without printing secret values.",
17927
+ rollback: { available: false, guidance: rollbackFor(plan, action.id) }
17928
+ });
17929
+ } else if (action.scope === "jira-profile") {
17930
+ const rotateProvider = parsed.boolFlags.has("--rotate-provider-token");
17931
+ if (!dryRun && (!current?.hasSecret || rotateProvider) && !secrets.values.azureDevOpsProviderToken)
17932
+ throw new SetupCommandError(
17933
+ "SECRET_INPUT_REQUIRED",
17934
+ "azureDevOpsProviderToken is required to create or rotate the Azure provider PAT.",
17935
+ ExitCode.ValidationError
17936
+ );
17937
+ const profile = dryRun ? current ?? {
17938
+ id: "<created-by-forge>",
17939
+ projectKey: plan.project.key,
17940
+ label: plan.jiraProfile.label,
17941
+ provider: "azureDevops",
17942
+ authType: "basicPat",
17943
+ method: "POST",
17944
+ endpointSummary: "<computed-by-forge>",
17945
+ config: plan.jiraProfile.config,
17946
+ enabled: plan.jiraProfile.enabled,
17947
+ hasSecret: false,
17948
+ createdAt: "",
17949
+ updatedAt: ""
17950
+ } : requireServiceData(
17951
+ await context.invokeForgeContract("upsertScenarioAutomationProfile", {
17952
+ context: { projectKey: plan.project.key },
17953
+ input: {
17954
+ profileId: current?.id,
17955
+ label: plan.jiraProfile.label,
17956
+ provider: "azureDevops",
17957
+ config: plan.jiraProfile.config,
17958
+ enabled: plan.jiraProfile.enabled,
17959
+ secretToken: !current?.hasSecret || rotateProvider ? secrets.values.azureDevOpsProviderToken : void 0
17960
+ }
17961
+ }),
17962
+ "upsertScenarioAutomationProfile"
17963
+ );
17964
+ appliedProfile = profile;
17965
+ results.push({
17966
+ id: action.id,
17967
+ scope: action.scope,
17968
+ status: dryRun ? "planned" : current ? "updated" : "created",
17969
+ message: "Azure DevOps Jira automation profile applied through the existing Forge contract.",
17970
+ rollback: { available: !dryRun, guidance: rollbackFor(plan, action.id) }
17971
+ });
17972
+ } else if (action.scope === "project-default" && appliedProfile && !dryRun) {
17973
+ requireServiceData(
17974
+ await context.invokeForgeContract("setScenarioAutomationDefaultProfile", {
17975
+ context: { projectKey: plan.project.key },
17976
+ profileId: appliedProfile.id
17977
+ }),
17978
+ "setScenarioAutomationDefaultProfile"
17979
+ );
17980
+ results.push({
17981
+ id: action.id,
17982
+ scope: action.scope,
17983
+ status: "updated",
17984
+ message: "Set the Azure DevOps Jira automation profile as project default.",
17985
+ rollback: { available: true, guidance: rollbackFor(plan, action.id) }
17986
+ });
17987
+ } else if (action.scope === "project-default") {
17988
+ results.push({
17989
+ id: action.id,
17990
+ scope: action.scope,
17991
+ status: "planned",
17992
+ message: "Set the Azure DevOps Jira automation profile as project default.",
17993
+ rollback: { available: false, guidance: rollbackFor(plan, action.id) }
17994
+ });
17995
+ }
17996
+ }
17997
+ return jsonResponse({
17998
+ schemaVersion: APPLY_RESULT_SCHEMA_VERSION,
17999
+ planId: plan.planId,
18000
+ provider: plan.provider,
18001
+ status: dryRun ? "dry-run" : "applied",
18002
+ dryRun,
18003
+ confirmed: !dryRun,
18004
+ approvedScopes: scopes,
18005
+ actions: results,
18006
+ smokeValidation: { status: "not-run", externalRunTriggered: false }
18007
+ });
18008
+ }
18009
+ async function doctorCommand(args, context, deps) {
18010
+ const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, /* @__PURE__ */ new Set(["--secrets-stdin"]), true);
17479
18011
  if (parsed.approvals.length > 0) {
17480
18012
  return jsonResponse(errorPayload("USAGE_ERROR", "setup doctor does not accept --approve."), ExitCode.UsageError);
17481
18013
  }
17482
18014
  if (parsed.errors.length > 0) {
17483
- return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors), ExitCode.UsageError);
18015
+ return jsonResponse(
18016
+ errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors),
18017
+ ExitCode.UsageError
18018
+ );
17484
18019
  }
17485
18020
  const planPath = parsed.flags["--plan"];
17486
18021
  if (!planPath) {
@@ -17490,6 +18025,64 @@ async function doctorCommand(args, context, deps) {
17490
18025
  try {
17491
18026
  const plan = loadPlan(planPath, deps.cwd);
17492
18027
  secrets = await resolveSecrets(parsed, deps);
18028
+ if (plan.provider === "azure-devops") {
18029
+ const checks = [];
18030
+ const inspection = await deps.azureProvider.inspectExecutionDefinition(
18031
+ plan,
18032
+ secrets.values.azureDevOpsProviderToken ?? secrets.values.azureDevOpsAdminToken
18033
+ );
18034
+ checks.push({
18035
+ id: "pipeline-azure",
18036
+ status: inspection.found && inspection.active === true ? "pass" : "fail",
18037
+ message: inspection.message
18038
+ });
18039
+ const variables = await deps.azureProvider.inspectCallbackSecretMetadata(
18040
+ plan,
18041
+ secrets.values.azureDevOpsAdminToken
18042
+ );
18043
+ const missing = plan.azureDevOps.requiredSecrets.filter(
18044
+ (item) => !variables.names.has(item.repositorySecretName)
18045
+ );
18046
+ checks.push({
18047
+ id: "callback-secret-metadata",
18048
+ status: variables.verified && missing.length === 0 ? "pass" : variables.verified ? "fail" : "warn",
18049
+ message: variables.message
18050
+ });
18051
+ const profiles = requireServiceData(
18052
+ await context.invokeForgeContract("listScenarioAutomationProfiles", {
18053
+ context: { projectKey: plan.project.key }
18054
+ }),
18055
+ "listScenarioAutomationProfiles"
18056
+ );
18057
+ const profile = profiles.find((item) => item.label === plan.jiraProfile.label);
18058
+ const config = profile?.config;
18059
+ 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;
18060
+ checks.push({
18061
+ id: "jira-profile",
18062
+ status: matches ? "pass" : "fail",
18063
+ 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."
18064
+ });
18065
+ if (plan.jiraProfile.setProjectDefault)
18066
+ checks.push({
18067
+ id: "jira-project-default",
18068
+ status: profile?.isProjectDefault ? "pass" : "fail",
18069
+ message: profile?.isProjectDefault ? "Jira automation profile is the project default." : "Jira automation profile is not the project default."
18070
+ });
18071
+ const status = doctorStatus(checks);
18072
+ const exitCode = status === "fail" ? ExitCode.ValidationError : ExitCode.Success;
18073
+ return jsonResponse(
18074
+ {
18075
+ schemaVersion: DOCTOR_RESULT_SCHEMA_VERSION,
18076
+ planId: plan.planId,
18077
+ provider: plan.provider,
18078
+ status,
18079
+ exitCode,
18080
+ externalRunTriggered: false,
18081
+ checks
18082
+ },
18083
+ exitCode
18084
+ );
18085
+ }
17493
18086
  const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
17494
18087
  const summary = await inspectPlan(plan, repoRoot, secrets, context, deps);
17495
18088
  return jsonResponse(summary, summary.exitCode);
@@ -17505,7 +18098,8 @@ function createSetupHandler(overrides = {}) {
17505
18098
  cwd: overrides.cwd ?? process.cwd(),
17506
18099
  env: overrides.env ?? process.env,
17507
18100
  readStdin: overrides.readStdin ?? defaultReadStdin,
17508
- githubProvider: overrides.githubProvider ?? new GitHubSetupProvider(overrides.githubProviderDeps)
18101
+ githubProvider: overrides.githubProvider ?? new GitHubSetupProvider(overrides.githubProviderDeps),
18102
+ azureProvider: overrides.azureProvider ?? new AzureDevOpsSetupProvider()
17509
18103
  };
17510
18104
  return async (request, context) => {
17511
18105
  const [subcommand, ...args] = request.args;
@@ -17609,10 +18203,7 @@ function suitesToLines(items) {
17609
18203
  if (items.length === 0) {
17610
18204
  return ["Test suites: 0", "No test suites found for the selected context."];
17611
18205
  }
17612
- return [
17613
- `Test suites: ${items.length}`,
17614
- ...items.map((item) => `- ${item.key} ${item.name}`)
17615
- ];
18206
+ return [`Test suites: ${items.length}`, ...items.map((item) => `- ${item.key} ${item.name}`)];
17616
18207
  }
17617
18208
  function suiteToLines(suite) {
17618
18209
  return [
@@ -18179,9 +18770,7 @@ function buildHelpText(registry = COMMAND_REGISTRY, experimentalSyncEnabled = fa
18179
18770
  "Available command groups:"
18180
18771
  ];
18181
18772
  for (const command of visibleCommands) {
18182
- lines.push(
18183
- ` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`
18184
- );
18773
+ lines.push(` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`);
18185
18774
  }
18186
18775
  lines.push(
18187
18776
  "",