@automatify-au/cli 0.1.10 → 0.1.12

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 +155 -1
  2. package/dist/automatify.cjs +2365 -275
  3. package/package.json +3 -2
@@ -2894,8 +2894,8 @@ var require_utils = __commonJS({
2894
2894
  var result = transform[inputType][outputType](input);
2895
2895
  return result;
2896
2896
  };
2897
- exports2.resolve = function(path12) {
2898
- var parts = path12.split("/");
2897
+ exports2.resolve = function(path16) {
2898
+ var parts = path16.split("/");
2899
2899
  var result = [];
2900
2900
  for (var index = 0; index < parts.length; index++) {
2901
2901
  var part = parts[index];
@@ -8748,18 +8748,18 @@ var require_object = __commonJS({
8748
8748
  var object = new ZipObject(name, zipObjectContent, o);
8749
8749
  this.files[name] = object;
8750
8750
  };
8751
- var parentFolder = function(path12) {
8752
- if (path12.slice(-1) === "/") {
8753
- path12 = path12.substring(0, path12.length - 1);
8751
+ var parentFolder = function(path16) {
8752
+ if (path16.slice(-1) === "/") {
8753
+ path16 = path16.substring(0, path16.length - 1);
8754
8754
  }
8755
- var lastSlash = path12.lastIndexOf("/");
8756
- return lastSlash > 0 ? path12.substring(0, lastSlash) : "";
8755
+ var lastSlash = path16.lastIndexOf("/");
8756
+ return lastSlash > 0 ? path16.substring(0, lastSlash) : "";
8757
8757
  };
8758
- var forceTrailingSlash = function(path12) {
8759
- if (path12.slice(-1) !== "/") {
8760
- path12 += "/";
8758
+ var forceTrailingSlash = function(path16) {
8759
+ if (path16.slice(-1) !== "/") {
8760
+ path16 += "/";
8761
8761
  }
8762
- return path12;
8762
+ return path16;
8763
8763
  };
8764
8764
  var folderAdd = function(name, createFolders) {
8765
8765
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -9725,9 +9725,9 @@ var require_load = __commonJS({
9725
9725
  var require_lib3 = __commonJS({
9726
9726
  "../../node_modules/.pnpm/jszip@3.10.1/node_modules/jszip/lib/index.js"(exports2, module2) {
9727
9727
  "use strict";
9728
- function JSZip2() {
9729
- if (!(this instanceof JSZip2)) {
9730
- return new JSZip2();
9728
+ function JSZip4() {
9729
+ if (!(this instanceof JSZip4)) {
9730
+ return new JSZip4();
9731
9731
  }
9732
9732
  if (arguments.length) {
9733
9733
  throw new Error("The constructor with parameters has been removed in JSZip 3.0, please check the upgrade guide.");
@@ -9736,7 +9736,7 @@ var require_lib3 = __commonJS({
9736
9736
  this.comment = null;
9737
9737
  this.root = "";
9738
9738
  this.clone = function() {
9739
- var newObj = new JSZip2();
9739
+ var newObj = new JSZip4();
9740
9740
  for (var i in this) {
9741
9741
  if (typeof this[i] !== "function") {
9742
9742
  newObj[i] = this[i];
@@ -9745,16 +9745,16 @@ var require_lib3 = __commonJS({
9745
9745
  return newObj;
9746
9746
  };
9747
9747
  }
9748
- JSZip2.prototype = require_object();
9749
- JSZip2.prototype.loadAsync = require_load();
9750
- JSZip2.support = require_support();
9751
- JSZip2.defaults = require_defaults();
9752
- JSZip2.version = "3.10.1";
9753
- JSZip2.loadAsync = function(content, options) {
9754
- return new JSZip2().loadAsync(content, options);
9748
+ JSZip4.prototype = require_object();
9749
+ JSZip4.prototype.loadAsync = require_load();
9750
+ JSZip4.support = require_support();
9751
+ JSZip4.defaults = require_defaults();
9752
+ JSZip4.version = "3.10.1";
9753
+ JSZip4.loadAsync = function(content, options) {
9754
+ return new JSZip4().loadAsync(content, options);
9755
9755
  };
9756
- JSZip2.external = require_external();
9757
- module2.exports = JSZip2;
9756
+ JSZip4.external = require_external();
9757
+ module2.exports = JSZip4;
9758
9758
  }
9759
9759
  });
9760
9760
 
@@ -10723,6 +10723,9 @@ function toJsonLine(data) {
10723
10723
  var import_node_fs4 = require("node:fs");
10724
10724
  var import_node_path5 = __toESM(require("node:path"), 1);
10725
10725
 
10726
+ // ../../packages/shared-types/dist/entitlements.js
10727
+ var UNLIMITED_ACTIVE_USERS = Number.MAX_SAFE_INTEGER;
10728
+
10726
10729
  // ../../packages/shared-types/dist/index.js
10727
10730
  var TestCaseStatus;
10728
10731
  (function(TestCaseStatus2) {
@@ -12447,10 +12450,10 @@ function createAutoHandler(deps = {}) {
12447
12450
  }
12448
12451
 
12449
12452
  // src/allure.ts
12450
- var import_node_path8 = __toESM(require("node:path"), 1);
12451
- var import_node_fs7 = require("node:fs");
12453
+ var import_node_path10 = __toESM(require("node:path"), 1);
12454
+ var import_node_fs9 = require("node:fs");
12452
12455
 
12453
- // ../../packages/jira-client/src/adf.ts
12456
+ // ../../packages/jira-client/dist/adf.js
12454
12457
  function paragraph(text = "") {
12455
12458
  return text ? { type: "paragraph", content: [{ type: "text", text }] } : { type: "paragraph" };
12456
12459
  }
@@ -12493,7 +12496,7 @@ function buildAllureEvidenceCommentAdf(input) {
12493
12496
  };
12494
12497
  }
12495
12498
 
12496
- // ../../packages/jira-client/src/jiraClient.ts
12499
+ // ../../packages/jira-client/dist/jiraClient.js
12497
12500
  var import_node_fs5 = require("node:fs");
12498
12501
  var import_node_path6 = __toESM(require("node:path"), 1);
12499
12502
  function normalizeSite(site) {
@@ -12619,7 +12622,7 @@ async function addIssueComment(params) {
12619
12622
  };
12620
12623
  }
12621
12624
 
12622
- // ../../packages/allure-core/src/parseAllureZip.ts
12625
+ // ../../packages/allure-core/dist/parseAllureZip.js
12623
12626
  var import_node_fs6 = require("node:fs");
12624
12627
  var import_node_path7 = __toESM(require("node:path"), 1);
12625
12628
  var import_jszip = __toESM(require_lib3(), 1);
@@ -12681,6 +12684,441 @@ async function parseAllureReportZip(zipPath) {
12681
12684
  };
12682
12685
  }
12683
12686
 
12687
+ // ../../packages/allure-core/dist/extractAllureZip.js
12688
+ var import_node_fs7 = require("node:fs");
12689
+ var import_node_path8 = __toESM(require("node:path"), 1);
12690
+ var import_jszip2 = __toESM(require_lib3(), 1);
12691
+ var INDEX_HTML = "index.html";
12692
+ var MISSING_INDEX_ERROR = "Could not find index.html. Please provide a generated Allure HTML report ZIP, not raw allure-results.";
12693
+ function findIndexEntry(paths) {
12694
+ return paths.find((entryPath) => entryPath === INDEX_HTML) ?? paths.find((entryPath) => entryPath === `allure-report/${INDEX_HTML}`) ?? paths.find((entryPath) => entryPath.endsWith(`/${INDEX_HTML}`));
12695
+ }
12696
+ function ensureInsideRoot(rootDir, candidate) {
12697
+ const resolved = import_node_path8.default.resolve(rootDir, candidate);
12698
+ const rootWithSep = rootDir.endsWith(import_node_path8.default.sep) ? rootDir : rootDir + import_node_path8.default.sep;
12699
+ if (resolved !== rootDir && !resolved.startsWith(rootWithSep)) {
12700
+ throw new Error(`Refusing to write zip entry outside extract directory: ${candidate}`);
12701
+ }
12702
+ return resolved;
12703
+ }
12704
+ async function extractAllureReportZip(zipPath, extractDir) {
12705
+ if (!(0, import_node_fs7.existsSync)(zipPath)) {
12706
+ throw new Error(`Allure report ZIP not found: ${zipPath}`);
12707
+ }
12708
+ if (import_node_path8.default.extname(zipPath).toLowerCase() !== ".zip") {
12709
+ throw new Error("Allure report must be a .zip file.");
12710
+ }
12711
+ const zip = await import_jszip2.default.loadAsync((0, import_node_fs7.readFileSync)(zipPath));
12712
+ const entries = Object.entries(zip.files);
12713
+ const filePaths = entries.filter(([, file]) => !file.dir).map(([entryPath]) => entryPath);
12714
+ const indexEntry = findIndexEntry(filePaths);
12715
+ if (!indexEntry) {
12716
+ throw new Error(MISSING_INDEX_ERROR);
12717
+ }
12718
+ const resolvedExtract = import_node_path8.default.resolve(extractDir);
12719
+ (0, import_node_fs7.mkdirSync)(resolvedExtract, { recursive: true });
12720
+ for (const [entryPath, file] of entries) {
12721
+ const target = ensureInsideRoot(resolvedExtract, entryPath);
12722
+ if (file.dir) {
12723
+ (0, import_node_fs7.mkdirSync)(target, { recursive: true });
12724
+ continue;
12725
+ }
12726
+ (0, import_node_fs7.mkdirSync)(import_node_path8.default.dirname(target), { recursive: true });
12727
+ const buffer = await file.async("nodebuffer");
12728
+ (0, import_node_fs7.writeFileSync)(target, buffer);
12729
+ }
12730
+ const indexAbsolute = import_node_path8.default.join(resolvedExtract, indexEntry);
12731
+ const rootDir = import_node_path8.default.dirname(indexAbsolute);
12732
+ return {
12733
+ zipPath,
12734
+ extractDir: resolvedExtract,
12735
+ rootDir,
12736
+ indexPath: indexAbsolute
12737
+ };
12738
+ }
12739
+
12740
+ // ../../packages/allure-core/dist/parseAllureFailedTests.js
12741
+ var import_jszip3 = __toESM(require_lib3(), 1);
12742
+
12743
+ // src/allureOpen.ts
12744
+ var import_node_fs8 = require("node:fs");
12745
+ var import_promises = require("node:fs/promises");
12746
+ var import_node_http = __toESM(require("node:http"), 1);
12747
+ var import_node_os = require("node:os");
12748
+ var import_node_path9 = __toESM(require("node:path"), 1);
12749
+ var import_node_child_process2 = require("node:child_process");
12750
+ function parseArgs2(args) {
12751
+ const positionals = [];
12752
+ const flags = {};
12753
+ const boolFlags = /* @__PURE__ */ new Set();
12754
+ const unknownFlags = [];
12755
+ const valuedFlags = /* @__PURE__ */ new Set(["--port", "--host", "--extract-to"]);
12756
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--keep", "--no-open", "--json", "--dry-run", "--verbose"]);
12757
+ for (let i = 0; i < args.length; i += 1) {
12758
+ const token = args[i];
12759
+ if (!token.startsWith("--")) {
12760
+ positionals.push(token);
12761
+ continue;
12762
+ }
12763
+ if (supportedBoolFlags.has(token)) {
12764
+ boolFlags.add(token);
12765
+ continue;
12766
+ }
12767
+ if (!valuedFlags.has(token)) {
12768
+ unknownFlags.push(token);
12769
+ continue;
12770
+ }
12771
+ const value = args[i + 1];
12772
+ if (!value || value.startsWith("--")) {
12773
+ unknownFlags.push(token);
12774
+ continue;
12775
+ }
12776
+ flags[token] = value;
12777
+ i += 1;
12778
+ }
12779
+ return { positionals, flags, boolFlags, unknownFlags };
12780
+ }
12781
+ function parsePort(raw) {
12782
+ if (raw === void 0) {
12783
+ return { value: 0 };
12784
+ }
12785
+ const parsed = Number(raw);
12786
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
12787
+ return { value: 0, error: `Invalid --port value: ${raw}. Must be an integer between 0 and 65535.` };
12788
+ }
12789
+ return { value: parsed };
12790
+ }
12791
+ function normalizeError(error) {
12792
+ return error instanceof Error ? error.message : "Unknown Allure open error.";
12793
+ }
12794
+ var MIME_TYPES = {
12795
+ ".html": "text/html; charset=utf-8",
12796
+ ".htm": "text/html; charset=utf-8",
12797
+ ".js": "application/javascript; charset=utf-8",
12798
+ ".mjs": "application/javascript; charset=utf-8",
12799
+ ".css": "text/css; charset=utf-8",
12800
+ ".json": "application/json; charset=utf-8",
12801
+ ".svg": "image/svg+xml",
12802
+ ".png": "image/png",
12803
+ ".jpg": "image/jpeg",
12804
+ ".jpeg": "image/jpeg",
12805
+ ".gif": "image/gif",
12806
+ ".webp": "image/webp",
12807
+ ".ico": "image/x-icon",
12808
+ ".woff": "font/woff",
12809
+ ".woff2": "font/woff2",
12810
+ ".ttf": "font/ttf",
12811
+ ".otf": "font/otf",
12812
+ ".eot": "application/vnd.ms-fontobject",
12813
+ ".map": "application/json; charset=utf-8",
12814
+ ".txt": "text/plain; charset=utf-8"
12815
+ };
12816
+ function contentTypeFor(filePath) {
12817
+ return MIME_TYPES[import_node_path9.default.extname(filePath).toLowerCase()] ?? "application/octet-stream";
12818
+ }
12819
+ function resolveRequestedPath(rootDir, urlPath) {
12820
+ let decoded;
12821
+ try {
12822
+ decoded = decodeURIComponent(urlPath.split("?")[0].split("#")[0]);
12823
+ } catch {
12824
+ return void 0;
12825
+ }
12826
+ const normalized = import_node_path9.default.normalize(decoded);
12827
+ const trimmed = normalized.replace(/^[/\\]+/, "");
12828
+ const candidate = import_node_path9.default.resolve(rootDir, trimmed);
12829
+ const rootWithSep = rootDir.endsWith(import_node_path9.default.sep) ? rootDir : rootDir + import_node_path9.default.sep;
12830
+ if (candidate !== rootDir && !candidate.startsWith(rootWithSep)) {
12831
+ return void 0;
12832
+ }
12833
+ return candidate;
12834
+ }
12835
+ function createStaticServerStarter() {
12836
+ return async ({ rootDir, host, port }) => {
12837
+ const server = import_node_http.default.createServer((req, res) => {
12838
+ if (!req.url || req.method !== "GET" && req.method !== "HEAD") {
12839
+ res.statusCode = 405;
12840
+ res.end("Method Not Allowed");
12841
+ return;
12842
+ }
12843
+ const resolved = resolveRequestedPath(rootDir, req.url);
12844
+ if (!resolved) {
12845
+ res.statusCode = 400;
12846
+ res.end("Bad Request");
12847
+ return;
12848
+ }
12849
+ let target = resolved;
12850
+ try {
12851
+ const stat = (0, import_node_fs8.statSync)(target);
12852
+ if (stat.isDirectory()) {
12853
+ target = import_node_path9.default.join(target, "index.html");
12854
+ }
12855
+ } catch {
12856
+ res.statusCode = 404;
12857
+ res.end("Not Found");
12858
+ return;
12859
+ }
12860
+ if (!(0, import_node_fs8.existsSync)(target)) {
12861
+ res.statusCode = 404;
12862
+ res.end("Not Found");
12863
+ return;
12864
+ }
12865
+ res.setHeader("Content-Type", contentTypeFor(target));
12866
+ if (req.method === "HEAD") {
12867
+ res.statusCode = 200;
12868
+ res.end();
12869
+ return;
12870
+ }
12871
+ const stream = (0, import_node_fs8.createReadStream)(target);
12872
+ stream.on("error", () => {
12873
+ res.statusCode = 500;
12874
+ res.end("Internal Server Error");
12875
+ });
12876
+ stream.pipe(res);
12877
+ });
12878
+ await new Promise((resolve, reject) => {
12879
+ const onError = (err) => {
12880
+ server.off("listening", onListening);
12881
+ reject(err);
12882
+ };
12883
+ const onListening = () => {
12884
+ server.off("error", onError);
12885
+ resolve();
12886
+ };
12887
+ server.once("error", onError);
12888
+ server.once("listening", onListening);
12889
+ server.listen(port, host);
12890
+ });
12891
+ const address = server.address();
12892
+ const actualPort = typeof address === "object" && address ? address.port : port;
12893
+ const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
12894
+ const url = `http://${displayHost}:${actualPort}/`;
12895
+ return {
12896
+ url,
12897
+ port: actualPort,
12898
+ host,
12899
+ close: () => new Promise((resolve, reject) => {
12900
+ server.close((err) => err ? reject(err) : resolve());
12901
+ })
12902
+ };
12903
+ };
12904
+ }
12905
+ function createOsBrowserOpener() {
12906
+ return async (url) => {
12907
+ const platform = process.platform;
12908
+ let command;
12909
+ let args;
12910
+ if (platform === "darwin") {
12911
+ command = "open";
12912
+ args = [url];
12913
+ } else if (platform === "win32") {
12914
+ command = "cmd";
12915
+ args = ["/c", "start", "", url];
12916
+ } else {
12917
+ command = "xdg-open";
12918
+ args = [url];
12919
+ }
12920
+ try {
12921
+ const child = (0, import_node_child_process2.spawn)(command, args, { stdio: "ignore", detached: true });
12922
+ child.on("error", () => void 0);
12923
+ child.unref();
12924
+ } catch {
12925
+ }
12926
+ };
12927
+ }
12928
+ function waitForProcessSignals() {
12929
+ return new Promise((resolve) => {
12930
+ const cleanup = () => {
12931
+ process.off("SIGINT", onSignal);
12932
+ process.off("SIGTERM", onSignal);
12933
+ resolve();
12934
+ };
12935
+ const onSignal = () => {
12936
+ cleanup();
12937
+ };
12938
+ process.once("SIGINT", onSignal);
12939
+ process.once("SIGTERM", onSignal);
12940
+ });
12941
+ }
12942
+ function buildJsonOutput(params) {
12943
+ const output = {
12944
+ zipPath: params.zipPath,
12945
+ dryRun: params.dryRun,
12946
+ served: params.served,
12947
+ opened: params.opened,
12948
+ keep: params.keep,
12949
+ extractDir: params.extractDir,
12950
+ rootDir: params.rootDir,
12951
+ indexPath: params.indexPath,
12952
+ summary: {
12953
+ total: params.summary.counts.total,
12954
+ passed: params.summary.counts.passed,
12955
+ failed: params.summary.counts.failed,
12956
+ broken: params.summary.counts.broken,
12957
+ skipped: params.summary.counts.skipped,
12958
+ unknown: params.summary.counts.unknown
12959
+ }
12960
+ };
12961
+ if (params.url) {
12962
+ output.url = params.url;
12963
+ }
12964
+ if (params.port !== void 0) {
12965
+ output.port = params.port;
12966
+ }
12967
+ if (params.host !== void 0) {
12968
+ output.host = params.host;
12969
+ }
12970
+ return output;
12971
+ }
12972
+ function createAllureOpenHandler(deps = {}) {
12973
+ const cwd = deps.cwd ?? process.cwd();
12974
+ const parseZip = deps.parseAllureReportZip ?? parseAllureReportZip;
12975
+ const extractZip = deps.extractAllureReportZip ?? extractAllureReportZip;
12976
+ const createTempDir = deps.createTempDir ?? (() => (0, import_promises.mkdtemp)(import_node_path9.default.join((0, import_node_os.tmpdir)(), "automatify-allure-open-")));
12977
+ const startServer = deps.startServer ?? createStaticServerStarter();
12978
+ const openInBrowser = deps.openInBrowser ?? createOsBrowserOpener();
12979
+ const waitForShutdown = deps.waitForShutdown ?? waitForProcessSignals;
12980
+ const removeDir = deps.removeDir ?? ((dir) => (0, import_promises.rm)(dir, { recursive: true, force: true }));
12981
+ const logger = deps.logger ?? console;
12982
+ return async (request) => {
12983
+ const parsed = parseArgs2(request.args);
12984
+ const useJson = parsed.boolFlags.has("--json");
12985
+ const dryRun = parsed.boolFlags.has("--dry-run");
12986
+ const verbose = parsed.boolFlags.has("--verbose");
12987
+ const keep = parsed.boolFlags.has("--keep");
12988
+ const noOpen = parsed.boolFlags.has("--no-open");
12989
+ const userExtractDir = parsed.flags["--extract-to"];
12990
+ if (parsed.unknownFlags.length > 0) {
12991
+ return {
12992
+ exitCode: ExitCode.UsageError,
12993
+ stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
12994
+ };
12995
+ }
12996
+ const [zipPathInput] = parsed.positionals;
12997
+ if (!zipPathInput || parsed.positionals.length > 1) {
12998
+ return {
12999
+ exitCode: ExitCode.UsageError,
13000
+ stderr: ["Usage: automatify testops allure open <zipPath> [options]"]
13001
+ };
13002
+ }
13003
+ const portResult = parsePort(parsed.flags["--port"]);
13004
+ if (portResult.error) {
13005
+ return {
13006
+ exitCode: ExitCode.UsageError,
13007
+ stderr: [`ERROR: ${portResult.error}`]
13008
+ };
13009
+ }
13010
+ const host = parsed.flags["--host"] ?? "127.0.0.1";
13011
+ const zipPath = import_node_path9.default.resolve(cwd, zipPathInput);
13012
+ let summary;
13013
+ try {
13014
+ summary = await parseZip(zipPath);
13015
+ } catch (error) {
13016
+ return {
13017
+ exitCode: ExitCode.ValidationError,
13018
+ stderr: [`ERROR: ${normalizeError(error)}`]
13019
+ };
13020
+ }
13021
+ const extractDirRaw = userExtractDir ? import_node_path9.default.resolve(cwd, userExtractDir) : await createTempDir();
13022
+ if (dryRun) {
13023
+ const indexPath = import_node_path9.default.join(extractDirRaw, "index.html");
13024
+ if (useJson) {
13025
+ return {
13026
+ 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
+ }))
13038
+ };
13039
+ }
13040
+ return {
13041
+ exitCode: ExitCode.Success,
13042
+ stdout: [
13043
+ `Allure report ZIP: ${zipPath}`,
13044
+ `Total: ${summary.counts.total} (passed ${summary.counts.passed}, failed ${summary.counts.failed}, broken ${summary.counts.broken}, skipped ${summary.counts.skipped}, unknown ${summary.counts.unknown})`,
13045
+ `Dry run: would extract to ${extractDirRaw} and serve on http://${host}:${portResult.value === 0 ? "<auto>" : portResult.value}/`
13046
+ ]
13047
+ };
13048
+ }
13049
+ let extractResult;
13050
+ try {
13051
+ extractResult = await extractZip(zipPath, extractDirRaw);
13052
+ } catch (error) {
13053
+ if (!userExtractDir) {
13054
+ await removeDir(extractDirRaw).catch(() => void 0);
13055
+ }
13056
+ return {
13057
+ exitCode: ExitCode.ValidationError,
13058
+ stderr: [`ERROR: ${normalizeError(error)}`]
13059
+ };
13060
+ }
13061
+ let serverHandle;
13062
+ try {
13063
+ serverHandle = await startServer({
13064
+ rootDir: extractResult.rootDir,
13065
+ host,
13066
+ port: portResult.value
13067
+ });
13068
+ } catch (error) {
13069
+ if (!keep && !userExtractDir) {
13070
+ await removeDir(extractResult.extractDir).catch(() => void 0);
13071
+ }
13072
+ return {
13073
+ exitCode: ExitCode.TransportError,
13074
+ stderr: [`ERROR: Could not start static server: ${normalizeError(error)}`]
13075
+ };
13076
+ }
13077
+ let opened = false;
13078
+ if (!noOpen) {
13079
+ try {
13080
+ await openInBrowser(serverHandle.url);
13081
+ opened = true;
13082
+ } catch {
13083
+ opened = false;
13084
+ }
13085
+ }
13086
+ 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
+ })));
13101
+ } else {
13102
+ logger.log(`Allure report extracted to ${extractResult.extractDir}.`);
13103
+ if (verbose) {
13104
+ logger.log(`Index: ${extractResult.indexPath}`);
13105
+ }
13106
+ logger.log(`Serving at ${serverHandle.url}`);
13107
+ logger.log(opened ? "Opened in your default browser." : "Browser open skipped.");
13108
+ logger.log("Press Ctrl+C to stop the server.");
13109
+ }
13110
+ await waitForShutdown();
13111
+ try {
13112
+ await serverHandle.close();
13113
+ } catch {
13114
+ }
13115
+ if (!keep && !userExtractDir) {
13116
+ await removeDir(extractResult.extractDir).catch(() => void 0);
13117
+ }
13118
+ return { exitCode: ExitCode.Success };
13119
+ };
13120
+ }
13121
+
12684
13122
  // src/allure.ts
12685
13123
  var CONFIG_FLAGS2 = /* @__PURE__ */ new Set([
12686
13124
  "--config",
@@ -12691,7 +13129,7 @@ var CONFIG_FLAGS2 = /* @__PURE__ */ new Set([
12691
13129
  "--jira-email",
12692
13130
  "--jira-api-token"
12693
13131
  ]);
12694
- function parseArgs2(args) {
13132
+ function parseArgs3(args) {
12695
13133
  const positionals = [];
12696
13134
  const flags = {};
12697
13135
  const boolFlags = /* @__PURE__ */ new Set();
@@ -12742,7 +13180,7 @@ function pickConfigArgs2(parsed) {
12742
13180
  function normalizeSite2(value) {
12743
13181
  return value.trim().replace(/\/+$/, "");
12744
13182
  }
12745
- function normalizeError(error) {
13183
+ function normalizeError2(error) {
12746
13184
  return error instanceof Error ? error.message : "Unknown Allure upload error.";
12747
13185
  }
12748
13186
  function hasCredential(value) {
@@ -12847,7 +13285,7 @@ function buildDownloadJsonOutput(params) {
12847
13285
  outputPath: params.outputPath
12848
13286
  };
12849
13287
  }
12850
- function buildJsonOutput(params) {
13288
+ function buildJsonOutput2(params) {
12851
13289
  const attachment = {
12852
13290
  filename: params.attachment.filename
12853
13291
  };
@@ -12887,19 +13325,24 @@ function createAllureHandler(deps = {}) {
12887
13325
  const createComment = deps.addIssueComment ?? addIssueComment;
12888
13326
  const listAttachments = deps.listIssueAttachments ?? listIssueAttachments;
12889
13327
  const downloadAttachment = deps.downloadIssueAttachment ?? downloadIssueAttachment;
12890
- return async (request) => {
13328
+ const openHandler = createAllureOpenHandler({ cwd });
13329
+ return async (request, context) => {
12891
13330
  const [subcommand, ...subArgs] = request.args;
13331
+ if (subcommand === "open") {
13332
+ return openHandler({ ...request, args: subArgs }, context);
13333
+ }
12892
13334
  if (subcommand !== "upload" && subcommand !== "download") {
12893
13335
  return {
12894
13336
  exitCode: ExitCode.UsageError,
12895
13337
  stderr: [
12896
13338
  "Usage:",
12897
13339
  " automatify testops allure upload <issueKey> <zipPath> [options]",
12898
- " automatify testops allure download <issueKey> [options]"
13340
+ " automatify testops allure download <issueKey> [options]",
13341
+ " automatify testops allure open <zipPath> [options]"
12899
13342
  ]
12900
13343
  };
12901
13344
  }
12902
- const parsed = parseArgs2(subArgs);
13345
+ const parsed = parseArgs3(subArgs);
12903
13346
  const useJson = parsed.boolFlags.has("--json");
12904
13347
  const dryRun = parsed.boolFlags.has("--dry-run");
12905
13348
  const verbose = parsed.boolFlags.has("--verbose");
@@ -12936,8 +13379,8 @@ function createAllureHandler(deps = {}) {
12936
13379
  attachmentId: parsed.flags["--attachment-id"],
12937
13380
  filename: parsed.flags["--filename"]
12938
13381
  });
12939
- const outputDir = import_node_path8.default.resolve(cwd, parsed.flags["--output-dir"] ?? ".");
12940
- const outputPath = import_node_path8.default.join(outputDir, selected.filename);
13382
+ const outputDir = import_node_path10.default.resolve(cwd, parsed.flags["--output-dir"] ?? ".");
13383
+ const outputPath = import_node_path10.default.join(outputDir, selected.filename);
12941
13384
  if (dryRun) {
12942
13385
  if (useJson) {
12943
13386
  return {
@@ -12960,8 +13403,8 @@ function createAllureHandler(deps = {}) {
12960
13403
  ]
12961
13404
  };
12962
13405
  }
12963
- (0, import_node_fs7.mkdirSync)(outputDir, { recursive: true });
12964
- if ((0, import_node_fs7.existsSync)(outputPath) && !force) {
13406
+ (0, import_node_fs9.mkdirSync)(outputDir, { recursive: true });
13407
+ if ((0, import_node_fs9.existsSync)(outputPath) && !force) {
12965
13408
  return {
12966
13409
  exitCode: ExitCode.ValidationError,
12967
13410
  stderr: [`ERROR: Output file already exists: ${outputPath}. Pass --force to overwrite.`]
@@ -12996,7 +13439,7 @@ function createAllureHandler(deps = {}) {
12996
13439
  } catch (error) {
12997
13440
  return {
12998
13441
  exitCode: ExitCode.RemoteError,
12999
- stderr: [`ERROR: ${normalizeError(error)}`]
13442
+ stderr: [`ERROR: ${normalizeError2(error)}`]
13000
13443
  };
13001
13444
  }
13002
13445
  }
@@ -13008,8 +13451,8 @@ function createAllureHandler(deps = {}) {
13008
13451
  };
13009
13452
  }
13010
13453
  const resolved = resolveJiraConfig(parsed, env, cwd);
13011
- const zipPath = import_node_path8.default.resolve(cwd, zipPathInput);
13012
- const attachmentFilename = import_node_path8.default.basename(zipPath);
13454
+ const zipPath = import_node_path10.default.resolve(cwd, zipPathInput);
13455
+ const attachmentFilename = import_node_path10.default.basename(zipPath);
13013
13456
  if (!resolved.config) {
13014
13457
  return {
13015
13458
  exitCode: ExitCode.ValidationError,
@@ -13022,14 +13465,14 @@ function createAllureHandler(deps = {}) {
13022
13465
  } catch (error) {
13023
13466
  return {
13024
13467
  exitCode: ExitCode.ValidationError,
13025
- stderr: [`ERROR: ${normalizeError(error)}`]
13468
+ stderr: [`ERROR: ${normalizeError2(error)}`]
13026
13469
  };
13027
13470
  }
13028
13471
  if (dryRun) {
13029
13472
  if (useJson) {
13030
13473
  return {
13031
13474
  exitCode: ExitCode.Success,
13032
- stdout: toJsonLine(buildJsonOutput({
13475
+ stdout: toJsonLine(buildJsonOutput2({
13033
13476
  issueKey,
13034
13477
  uploaded: false,
13035
13478
  commentAdded: false,
@@ -13072,7 +13515,7 @@ function createAllureHandler(deps = {}) {
13072
13515
  if (useJson) {
13073
13516
  return {
13074
13517
  exitCode: ExitCode.Success,
13075
- stdout: toJsonLine(buildJsonOutput({
13518
+ stdout: toJsonLine(buildJsonOutput2({
13076
13519
  issueKey,
13077
13520
  uploaded: true,
13078
13521
  commentAdded,
@@ -13096,15 +13539,15 @@ function createAllureHandler(deps = {}) {
13096
13539
  } catch (error) {
13097
13540
  return {
13098
13541
  exitCode: ExitCode.RemoteError,
13099
- stderr: [`ERROR: ${normalizeError(error)}`]
13542
+ stderr: [`ERROR: ${normalizeError2(error)}`]
13100
13543
  };
13101
13544
  }
13102
13545
  };
13103
13546
  }
13104
13547
 
13105
13548
  // src/bdd.ts
13106
- var import_node_fs8 = require("node:fs");
13107
- var import_node_path9 = __toESM(require("node:path"), 1);
13549
+ var import_node_fs10 = require("node:fs");
13550
+ var import_node_path11 = __toESM(require("node:path"), 1);
13108
13551
  var import_yazl = __toESM(require_yazl(), 1);
13109
13552
 
13110
13553
  // src/forgeClient.ts
@@ -13269,7 +13712,7 @@ var CONFIG_FLAGS3 = /* @__PURE__ */ new Set([
13269
13712
  "--jira-email",
13270
13713
  "--jira-api-token"
13271
13714
  ]);
13272
- function parseArgs3(args) {
13715
+ function parseArgs4(args) {
13273
13716
  const flags = {};
13274
13717
  const boolFlags = /* @__PURE__ */ new Set();
13275
13718
  const unknownFlags = [];
@@ -13316,7 +13759,7 @@ function pickConfigArgs3(parsed) {
13316
13759
  }
13317
13760
  return args;
13318
13761
  }
13319
- function normalizeError2(error) {
13762
+ function normalizeError3(error) {
13320
13763
  if (error instanceof ForgeClientError) {
13321
13764
  return `${error.code}: ${error.message}`;
13322
13765
  }
@@ -13408,7 +13851,7 @@ function parseScenarioFeatureFile(raw, filePath) {
13408
13851
  }
13409
13852
  const scenarioTag = tags.map((tag) => tag.match(SCENARIO_METADATA_PATTERN)).find((match) => Boolean(match));
13410
13853
  const scenarioKeyFromTag = scenarioTag?.[1]?.trim().toUpperCase() ?? "";
13411
- const fileKeyMatch = import_node_path9.default.basename(filePath ?? "").match(/^(SC-\d+)\.feature$/i);
13854
+ const fileKeyMatch = import_node_path11.default.basename(filePath ?? "").match(/^(SC-\d+)\.feature$/i);
13412
13855
  const scenarioKeyFromFile = fileKeyMatch?.[1]?.trim().toUpperCase() ?? "";
13413
13856
  const scenarioKey = scenarioKeyFromTag || scenarioKeyFromFile;
13414
13857
  if (scenarioKeyFromTag && scenarioKeyFromFile && scenarioKeyFromTag !== scenarioKeyFromFile) {
@@ -13526,7 +13969,7 @@ function toJsonExportManifestItems(items) {
13526
13969
  async function defaultCreateZipArchive(archivePath, entries) {
13527
13970
  await new Promise((resolve, reject) => {
13528
13971
  const zip = new import_yazl.ZipFile();
13529
- const output = zip.outputStream.pipe((0, import_node_fs8.createWriteStream)(archivePath));
13972
+ const output = zip.outputStream.pipe((0, import_node_fs10.createWriteStream)(archivePath));
13530
13973
  output.on("close", () => resolve());
13531
13974
  output.on("error", reject);
13532
13975
  zip.outputStream.on("error", reject);
@@ -13556,14 +13999,14 @@ function missingProjectResponse() {
13556
13999
  function createBddHandler(deps = {}) {
13557
14000
  const cwd = deps.cwd ?? process.cwd();
13558
14001
  const env = deps.env ?? process.env;
13559
- const mkdir = deps.mkdir ?? ((targetPath, options) => (0, import_node_fs8.mkdirSync)(targetPath, options));
13560
- const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs8.readFileSync)(filePath, "utf8"));
13561
- const readDir = deps.readDir ?? ((dirPath) => (0, import_node_fs8.readdirSync)(dirPath));
13562
- const writeFile = deps.writeFile ?? ((filePath, content) => (0, import_node_fs8.writeFileSync)(filePath, content, "utf8"));
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"));
13563
14006
  const createZipArchive = deps.createZipArchive ?? defaultCreateZipArchive;
13564
14007
  return async (request, context) => {
13565
14008
  const [subcommand, ...restArgs] = request.args;
13566
- const parsed = parseArgs3(restArgs);
14009
+ const parsed = parseArgs4(restArgs);
13567
14010
  const requestedFormat = (parsed.flags["--format"] ?? "").trim().toLowerCase();
13568
14011
  const outputFormat = parsed.boolFlags.has("--json") ? "json" : parsed.boolFlags.has("--feature") ? "feature" : requestedFormat || "text";
13569
14012
  const config = resolveCliConfig(pickConfigArgs3(parsed), env, cwd);
@@ -13603,7 +14046,7 @@ function createBddHandler(deps = {}) {
13603
14046
  stderr: ["ERROR: Missing required --output-dir for testops bdd features export."]
13604
14047
  };
13605
14048
  }
13606
- const outputDir = import_node_path9.default.resolve(cwd, outputDirFlag);
14049
+ const outputDir = import_node_path11.default.resolve(cwd, outputDirFlag);
13607
14050
  try {
13608
14051
  const result = await context.invokeForgeContract("listBddFeatures", {
13609
14052
  context: {
@@ -13639,7 +14082,7 @@ function createBddHandler(deps = {}) {
13639
14082
  `
13640
14083
  }));
13641
14084
  for (let index = 0; index < features.length; index += 1) {
13642
- writeFile(import_node_path9.default.join(outputDir, fileNames[index]), exportEntries[index].content);
14085
+ writeFile(import_node_path11.default.join(outputDir, fileNames[index]), exportEntries[index].content);
13643
14086
  }
13644
14087
  const manifest = {
13645
14088
  action: "bdd-features-export",
@@ -13652,7 +14095,7 @@ function createBddHandler(deps = {}) {
13652
14095
  };
13653
14096
  const manifestContent = `${JSON.stringify(manifest, null, 2)}
13654
14097
  `;
13655
- writeFile(import_node_path9.default.join(outputDir, "manifest.json"), manifestContent);
14098
+ writeFile(import_node_path11.default.join(outputDir, "manifest.json"), manifestContent);
13656
14099
  const archivePath = `${outputDir}.zip`;
13657
14100
  if (zipRequested) {
13658
14101
  await createZipArchive(archivePath, [
@@ -13691,7 +14134,7 @@ function createBddHandler(deps = {}) {
13691
14134
  } catch (error) {
13692
14135
  return {
13693
14136
  exitCode: ExitCode.TransportError,
13694
- stderr: [`ERROR: ${normalizeError2(error)}`]
14137
+ stderr: [`ERROR: ${normalizeError3(error)}`]
13695
14138
  };
13696
14139
  }
13697
14140
  }
@@ -13722,7 +14165,7 @@ function createBddHandler(deps = {}) {
13722
14165
  stderr: ["ERROR: Missing required --output-dir for testops bdd scenarios export."]
13723
14166
  };
13724
14167
  }
13725
- const outputDir = import_node_path9.default.resolve(cwd, outputDirFlag);
14168
+ const outputDir = import_node_path11.default.resolve(cwd, outputDirFlag);
13726
14169
  const zipRequested = parsed.boolFlags.has("--zip");
13727
14170
  try {
13728
14171
  const scenariosResult = await context.invokeForgeContract("listBddScenarios", {
@@ -13757,7 +14200,7 @@ function createBddHandler(deps = {}) {
13757
14200
  const fileName = scenarioToExportFileName(scenario);
13758
14201
  const fileContent = `${scenarioToFeatureLines(scenario).join("\n")}
13759
14202
  `;
13760
- writeFile(import_node_path9.default.join(outputDir, fileName), fileContent);
14203
+ writeFile(import_node_path11.default.join(outputDir, fileName), fileContent);
13761
14204
  return {
13762
14205
  scenario,
13763
14206
  fileName,
@@ -13783,7 +14226,7 @@ function createBddHandler(deps = {}) {
13783
14226
  };
13784
14227
  const manifestContent = `${JSON.stringify(manifest, null, 2)}
13785
14228
  `;
13786
- writeFile(import_node_path9.default.join(outputDir, "manifest.json"), manifestContent);
14229
+ writeFile(import_node_path11.default.join(outputDir, "manifest.json"), manifestContent);
13787
14230
  const archivePath = `${outputDir}.zip`;
13788
14231
  if (zipRequested) {
13789
14232
  await createZipArchive(archivePath, [
@@ -13826,13 +14269,13 @@ function createBddHandler(deps = {}) {
13826
14269
  } catch (error) {
13827
14270
  return {
13828
14271
  exitCode: ExitCode.TransportError,
13829
- stderr: [`ERROR: ${normalizeError2(error)}`]
14272
+ stderr: [`ERROR: ${normalizeError3(error)}`]
13830
14273
  };
13831
14274
  }
13832
14275
  }
13833
14276
  if (nested === "import") {
13834
- const sourceFile = parsed.flags["--file"] ? import_node_path9.default.resolve(cwd, parsed.flags["--file"]) : "";
13835
- const inputDir = parsed.flags["--input-dir"] ? import_node_path9.default.resolve(cwd, parsed.flags["--input-dir"]) : "";
14277
+ const sourceFile = parsed.flags["--file"] ? import_node_path11.default.resolve(cwd, parsed.flags["--file"]) : "";
14278
+ const inputDir = parsed.flags["--input-dir"] ? import_node_path11.default.resolve(cwd, parsed.flags["--input-dir"]) : "";
13836
14279
  const dryRun = parsed.boolFlags.has("--dry-run");
13837
14280
  if (!sourceFile && !inputDir) {
13838
14281
  return {
@@ -13841,7 +14284,7 @@ function createBddHandler(deps = {}) {
13841
14284
  };
13842
14285
  }
13843
14286
  try {
13844
- const scenarioFiles = sourceFile ? [sourceFile] : readDir(inputDir).filter((entry) => entry.toLowerCase().endsWith(".feature")).map((entry) => import_node_path9.default.join(inputDir, entry)).sort((left, right) => left.localeCompare(right));
14287
+ const scenarioFiles = sourceFile ? [sourceFile] : readDir(inputDir).filter((entry) => entry.toLowerCase().endsWith(".feature")).map((entry) => import_node_path11.default.join(inputDir, entry)).sort((left, right) => left.localeCompare(right));
13845
14288
  if (scenarioFiles.length === 0) {
13846
14289
  return {
13847
14290
  exitCode: ExitCode.ValidationError,
@@ -13980,7 +14423,7 @@ function createBddHandler(deps = {}) {
13980
14423
  } catch (error) {
13981
14424
  return {
13982
14425
  exitCode: ExitCode.TransportError,
13983
- stderr: [`ERROR: ${normalizeError2(error)}`]
14426
+ stderr: [`ERROR: ${normalizeError3(error)}`]
13984
14427
  };
13985
14428
  }
13986
14429
  }
@@ -14083,7 +14526,7 @@ function createBddHandler(deps = {}) {
14083
14526
  } catch (error) {
14084
14527
  return {
14085
14528
  exitCode: ExitCode.TransportError,
14086
- stderr: [`ERROR: ${normalizeError2(error)}`]
14529
+ stderr: [`ERROR: ${normalizeError3(error)}`]
14087
14530
  };
14088
14531
  }
14089
14532
  }
@@ -14136,7 +14579,7 @@ function createBddHandler(deps = {}) {
14136
14579
  } catch (error) {
14137
14580
  return {
14138
14581
  exitCode: ExitCode.TransportError,
14139
- stderr: [`ERROR: ${normalizeError2(error)}`]
14582
+ stderr: [`ERROR: ${normalizeError3(error)}`]
14140
14583
  };
14141
14584
  }
14142
14585
  }
@@ -14157,7 +14600,7 @@ var CONFIG_FLAGS4 = /* @__PURE__ */ new Set([
14157
14600
  "--jira-email",
14158
14601
  "--jira-api-token"
14159
14602
  ]);
14160
- function parseArgs4(args) {
14603
+ function parseArgs5(args) {
14161
14604
  const flags = {};
14162
14605
  const boolFlags = /* @__PURE__ */ new Set();
14163
14606
  const unknownFlags = [];
@@ -14195,7 +14638,7 @@ function pickConfigArgs4(parsed) {
14195
14638
  }
14196
14639
  return args;
14197
14640
  }
14198
- function normalizeError3(error) {
14641
+ function normalizeError4(error) {
14199
14642
  if (error instanceof ForgeClientError) {
14200
14643
  return `${error.code}: ${error.message}`;
14201
14644
  }
@@ -14329,7 +14772,7 @@ function createCasesHandler(deps = {}) {
14329
14772
  const env = deps.env ?? process.env;
14330
14773
  return async (request, context) => {
14331
14774
  const [subcommand, ...restArgs] = request.args;
14332
- const parsed = parseArgs4(restArgs);
14775
+ const parsed = parseArgs5(restArgs);
14333
14776
  const useJson = parsed.boolFlags.has("--json");
14334
14777
  const config = resolveCliConfig(pickConfigArgs4(parsed), env, cwd);
14335
14778
  const projectKey = config.values.projectKey;
@@ -14376,7 +14819,7 @@ function createCasesHandler(deps = {}) {
14376
14819
  } catch (error) {
14377
14820
  return {
14378
14821
  exitCode: ExitCode.TransportError,
14379
- stderr: [`ERROR: ${normalizeError3(error)}`]
14822
+ stderr: [`ERROR: ${normalizeError4(error)}`]
14380
14823
  };
14381
14824
  }
14382
14825
  }
@@ -14418,7 +14861,7 @@ function createCasesHandler(deps = {}) {
14418
14861
  } catch (error) {
14419
14862
  return {
14420
14863
  exitCode: ExitCode.TransportError,
14421
- stderr: [`ERROR: ${normalizeError3(error)}`]
14864
+ stderr: [`ERROR: ${normalizeError4(error)}`]
14422
14865
  };
14423
14866
  }
14424
14867
  }
@@ -14481,7 +14924,7 @@ function createCasesHandler(deps = {}) {
14481
14924
  } catch (error) {
14482
14925
  return {
14483
14926
  exitCode: ExitCode.TransportError,
14484
- stderr: [`ERROR: ${normalizeError3(error)}`]
14927
+ stderr: [`ERROR: ${normalizeError4(error)}`]
14485
14928
  };
14486
14929
  }
14487
14930
  }
@@ -14544,7 +14987,7 @@ function createCasesHandler(deps = {}) {
14544
14987
  } catch (error) {
14545
14988
  return {
14546
14989
  exitCode: ExitCode.TransportError,
14547
- stderr: [`ERROR: ${normalizeError3(error)}`]
14990
+ stderr: [`ERROR: ${normalizeError4(error)}`]
14548
14991
  };
14549
14992
  }
14550
14993
  }
@@ -14556,6 +14999,7 @@ function createCasesHandler(deps = {}) {
14556
14999
  }
14557
15000
 
14558
15001
  // src/doctor.ts
15002
+ var import_node_child_process3 = require("node:child_process");
14559
15003
  var CONFIG_FLAGS5 = /* @__PURE__ */ new Set([
14560
15004
  "--config",
14561
15005
  "--base-url",
@@ -14565,11 +15009,16 @@ var CONFIG_FLAGS5 = /* @__PURE__ */ new Set([
14565
15009
  "--jira-email",
14566
15010
  "--jira-api-token"
14567
15011
  ]);
14568
- function parseArgs5(args) {
15012
+ var DIRECT_JIRA_FLAGS = /* @__PURE__ */ new Set([
15013
+ "--site",
15014
+ "--email",
15015
+ "--api-token"
15016
+ ]);
15017
+ function parseArgs6(args) {
14569
15018
  const flags = {};
14570
15019
  const boolFlags = /* @__PURE__ */ new Set();
14571
15020
  const unknownFlags = [];
14572
- const valuedFlags = /* @__PURE__ */ new Set([...CONFIG_FLAGS5]);
15021
+ const valuedFlags = /* @__PURE__ */ new Set([...CONFIG_FLAGS5, ...DIRECT_JIRA_FLAGS]);
14573
15022
  const supportedBoolFlags = /* @__PURE__ */ new Set(["--json", "--check-context"]);
14574
15023
  for (let i = 0; i < args.length; i += 1) {
14575
15024
  const token = args[i];
@@ -14635,6 +15084,49 @@ function checkFromServiceResult(result) {
14635
15084
  message: `Forge endpoint reachable (service returned ${result.error.code}).`
14636
15085
  };
14637
15086
  }
15087
+ function normalizeSite3(value) {
15088
+ return value.trim().replace(/\/+$/, "");
15089
+ }
15090
+ function hasValue(value) {
15091
+ return value.trim().length > 0;
15092
+ }
15093
+ function resolveAllureJiraConfig(parsed, env, cwd) {
15094
+ const resolution = resolveCliConfig(pickConfigArgs5(parsed), env, cwd);
15095
+ const site = normalizeSite3(parsed.flags["--site"] ?? env.JIRA_SITE ?? resolution.values.baseUrl);
15096
+ const email = (parsed.flags["--email"] ?? resolution.values.jiraEmail).trim();
15097
+ const apiToken = (parsed.flags["--api-token"] ?? resolution.values.jiraApiToken).trim();
15098
+ const issueKey = (parsed.flags["--issue-key"] ?? "").trim();
15099
+ const errors = [];
15100
+ if (!hasValue(site)) {
15101
+ errors.push("Missing Jira site. Pass --site, set JIRA_SITE/JIRA_BASE_URL, or configure baseUrl.");
15102
+ }
15103
+ if (!hasValue(email)) {
15104
+ errors.push("Missing Jira email. Pass --email or set JIRA_EMAIL.");
15105
+ }
15106
+ if (!hasValue(apiToken)) {
15107
+ errors.push("Missing Jira API token. Pass --api-token or set JIRA_API_TOKEN.");
15108
+ }
15109
+ return { site, email, apiToken, issueKey, errors };
15110
+ }
15111
+ function commandCheck(deps, command, args, passMessage, warnMessage) {
15112
+ try {
15113
+ deps.execFileSync(command, [...args], {
15114
+ encoding: "utf8",
15115
+ stdio: ["ignore", "pipe", "pipe"]
15116
+ });
15117
+ return {
15118
+ name: command,
15119
+ status: "pass",
15120
+ message: passMessage
15121
+ };
15122
+ } catch {
15123
+ return {
15124
+ name: command,
15125
+ status: "warn",
15126
+ message: warnMessage
15127
+ };
15128
+ }
15129
+ }
14638
15130
  function toJsonPayload(summary) {
14639
15131
  return {
14640
15132
  status: summary.status,
@@ -14667,9 +15159,11 @@ function toHumanLines2(summary) {
14667
15159
  function createDoctorHandler(deps = {}) {
14668
15160
  const cwd = deps.cwd ?? process.cwd();
14669
15161
  const env = deps.env ?? process.env;
15162
+ const runCommand = deps.execFileSync ?? import_node_child_process3.execFileSync;
15163
+ const listAttachments = deps.listIssueAttachments ?? listIssueAttachments;
14670
15164
  return async (request, context) => {
14671
- const [, ...subArgs] = request.args;
14672
- const parsed = parseArgs5(subArgs);
15165
+ const [subcommand, ...subArgs] = request.args;
15166
+ const parsed = parseArgs6(subArgs);
14673
15167
  const useJson = parsed.boolFlags.has("--json");
14674
15168
  const enforceContextCheck = parsed.boolFlags.has("--check-context");
14675
15169
  const checks = [];
@@ -14685,10 +15179,74 @@ function createDoctorHandler(deps = {}) {
14685
15179
  stdout: useJson ? toJsonLine(toJsonPayload(summary2)) : toHumanLines2(summary2)
14686
15180
  };
14687
15181
  }
14688
- const configArgs = pickConfigArgs5(parsed);
14689
- const resolution = resolveCliConfig(configArgs, env, cwd);
14690
- const configValidation = validateCliConfigResolution(resolution);
14691
- checks.push({
15182
+ if (subcommand === "allure") {
15183
+ const config = resolveAllureJiraConfig(parsed, env, cwd);
15184
+ checks.push({
15185
+ name: "allure_jira_credentials",
15186
+ status: config.errors.length === 0 ? "pass" : "fail",
15187
+ message: config.errors.length === 0 ? "Jira site, email, and API token are configured for Allure upload/download." : config.errors.join(" ")
15188
+ });
15189
+ if (config.issueKey && config.errors.length === 0) {
15190
+ try {
15191
+ const attachments = await listAttachments({
15192
+ site: config.site,
15193
+ email: config.email,
15194
+ apiToken: config.apiToken,
15195
+ issueKey: config.issueKey
15196
+ });
15197
+ checks.push({
15198
+ name: "allure_jira_issue_access",
15199
+ status: "pass",
15200
+ message: `Jira issue ${config.issueKey} is reachable; ${attachments.length} attachment(s) visible.`
15201
+ });
15202
+ } catch (error) {
15203
+ checks.push({
15204
+ name: "allure_jira_issue_access",
15205
+ status: "fail",
15206
+ message: normalizeConnectivityError(error)
15207
+ });
15208
+ }
15209
+ } else if (!config.issueKey) {
15210
+ checks.push({
15211
+ name: "allure_jira_issue_access",
15212
+ status: "skip",
15213
+ message: "Issue access check skipped. Pass --issue-key to verify Jira read/download access for a specific issue."
15214
+ });
15215
+ }
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
+ ));
15237
+ const summary2 = {
15238
+ status: computeOverallStatus(checks),
15239
+ checks
15240
+ };
15241
+ return {
15242
+ exitCode: pickDoctorExitCode(summary2),
15243
+ stdout: useJson ? toJsonLine(toJsonPayload(summary2)) : toHumanLines2(summary2)
15244
+ };
15245
+ }
15246
+ const configArgs = pickConfigArgs5(parsed);
15247
+ const resolution = resolveCliConfig(configArgs, env, cwd);
15248
+ const configValidation = validateCliConfigResolution(resolution);
15249
+ checks.push({
14692
15250
  name: "config_completeness",
14693
15251
  status: configValidation.ok ? "pass" : "fail",
14694
15252
  message: configValidation.ok ? "Required config is present." : configValidation.errors.join(" ")
@@ -14773,8 +15331,8 @@ function createDoctorHandler(deps = {}) {
14773
15331
  }
14774
15332
 
14775
15333
  // src/ingestFeature.ts
14776
- var import_node_fs9 = require("node:fs");
14777
- var import_node_path10 = __toESM(require("node:path"), 1);
15334
+ var import_node_fs11 = require("node:fs");
15335
+ var import_node_path12 = __toESM(require("node:path"), 1);
14778
15336
  var CONFIG_FLAGS6 = /* @__PURE__ */ new Set([
14779
15337
  "--config",
14780
15338
  "--base-url",
@@ -14784,7 +15342,7 @@ var CONFIG_FLAGS6 = /* @__PURE__ */ new Set([
14784
15342
  "--jira-email",
14785
15343
  "--jira-api-token"
14786
15344
  ]);
14787
- function parseArgs6(args) {
15345
+ function parseArgs7(args) {
14788
15346
  const flags = {};
14789
15347
  const boolFlags = /* @__PURE__ */ new Set();
14790
15348
  const unknownFlags = [];
@@ -14850,7 +15408,7 @@ function summarizeSuccess(result) {
14850
15408
  }
14851
15409
  return lines;
14852
15410
  }
14853
- function normalizeError4(error) {
15411
+ function normalizeError5(error) {
14854
15412
  if (error instanceof ForgeClientError) {
14855
15413
  return `${error.code}: ${error.message}`;
14856
15414
  }
@@ -14860,18 +15418,18 @@ function normalizeError4(error) {
14860
15418
  return "Unknown ingest error.";
14861
15419
  }
14862
15420
  function createIngestFeatureHandler(deps = {}) {
14863
- const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs9.readFileSync)(filePath, "utf8"));
14864
- const readStdin = deps.readStdin ?? (() => (0, import_node_fs9.readFileSync)(0, "utf8"));
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"));
14865
15423
  const cwd = deps.cwd ?? process.cwd();
14866
15424
  const env = deps.env ?? process.env;
14867
15425
  return async (request, context) => {
14868
15426
  const [, ...subArgs] = request.args;
14869
- const parsed = parseArgs6(subArgs);
15427
+ const parsed = parseArgs7(subArgs);
14870
15428
  const configArgs = pickConfigArgs6(parsed);
14871
15429
  const config = resolveCliConfig(configArgs, env, cwd);
14872
15430
  const projectKey = config.values.projectKey;
14873
15431
  const issueKey = config.values.issueKey;
14874
- const sourceFile = parsed.flags["--file"] ? import_node_path10.default.resolve(cwd, parsed.flags["--file"]) : "";
15432
+ const sourceFile = parsed.flags["--file"] ? import_node_path12.default.resolve(cwd, parsed.flags["--file"]) : "";
14875
15433
  const useStdin = parsed.boolFlags.has("--stdin");
14876
15434
  const useJson = parsed.boolFlags.has("--json");
14877
15435
  const sourceCount = Number(Boolean(sourceFile)) + Number(useStdin);
@@ -14887,7 +15445,7 @@ function createIngestFeatureHandler(deps = {}) {
14887
15445
  } catch (error) {
14888
15446
  return {
14889
15447
  exitCode: ExitCode.InternalError,
14890
- stderr: [`ERROR: Failed to read feature source: ${normalizeError4(error)}`]
15448
+ stderr: [`ERROR: Failed to read feature source: ${normalizeError5(error)}`]
14891
15449
  };
14892
15450
  }
14893
15451
  const name = (parsed.flags["--name"] ?? deriveNameFromGherkin(gherkin)).trim();
@@ -14982,7 +15540,7 @@ function createIngestFeatureHandler(deps = {}) {
14982
15540
  status: "failed",
14983
15541
  featureName: name,
14984
15542
  source: useStdin ? "stdin" : sourceFile,
14985
- errorMessage: normalizeError4(error)
15543
+ errorMessage: normalizeError5(error)
14986
15544
  })
14987
15545
  };
14988
15546
  }
@@ -14993,15 +15551,15 @@ function createIngestFeatureHandler(deps = {}) {
14993
15551
  `Feature: ${name}`,
14994
15552
  `Source: ${useStdin ? "stdin" : sourceFile}`
14995
15553
  ],
14996
- stderr: [`ERROR: ${normalizeError4(error)}`]
15554
+ stderr: [`ERROR: ${normalizeError5(error)}`]
14997
15555
  };
14998
15556
  }
14999
15557
  };
15000
15558
  }
15001
15559
 
15002
15560
  // src/runUpload.ts
15003
- var import_node_fs10 = require("node:fs");
15004
- var import_node_path11 = __toESM(require("node:path"), 1);
15561
+ var import_node_fs12 = require("node:fs");
15562
+ var import_node_path13 = __toESM(require("node:path"), 1);
15005
15563
  var STEP_RESULTS = [
15006
15564
  StepResult.Passed,
15007
15565
  StepResult.Failed,
@@ -15017,7 +15575,7 @@ var CONFIG_FLAGS7 = /* @__PURE__ */ new Set([
15017
15575
  "--jira-email",
15018
15576
  "--jira-api-token"
15019
15577
  ]);
15020
- function parseArgs7(args) {
15578
+ function parseArgs8(args) {
15021
15579
  const flags = {};
15022
15580
  const boolFlags = /* @__PURE__ */ new Set();
15023
15581
  const unknownFlags = [];
@@ -15063,7 +15621,7 @@ function pickConfigArgs7(parsed) {
15063
15621
  }
15064
15622
  return configArgs;
15065
15623
  }
15066
- function normalizeError5(error) {
15624
+ function normalizeError6(error) {
15067
15625
  if (error instanceof ForgeClientError) {
15068
15626
  return `${error.code}: ${error.message}`;
15069
15627
  }
@@ -15142,19 +15700,19 @@ function summarizeRunResult(result) {
15142
15700
  return lines;
15143
15701
  }
15144
15702
  function createRunUploadHandler(deps = {}) {
15145
- const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs10.readFileSync)(filePath, "utf8"));
15146
- const readStdin = deps.readStdin ?? (() => (0, import_node_fs10.readFileSync)(0, "utf8"));
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"));
15147
15705
  const cwd = deps.cwd ?? process.cwd();
15148
15706
  const env = deps.env ?? process.env;
15149
15707
  return async (request, context) => {
15150
15708
  const [, ...subArgs] = request.args;
15151
- const parsed = parseArgs7(subArgs);
15709
+ const parsed = parseArgs8(subArgs);
15152
15710
  const configArgs = pickConfigArgs7(parsed);
15153
15711
  const config = resolveCliConfig(configArgs, env, cwd);
15154
15712
  const projectKey = config.values.projectKey;
15155
15713
  const issueKey = config.values.issueKey;
15156
15714
  const testCaseKey = parsed.flags["--test-case-key"]?.trim().toUpperCase() ?? "";
15157
- const sourceFile = parsed.flags["--file"] ? import_node_path11.default.resolve(cwd, parsed.flags["--file"]) : "";
15715
+ const sourceFile = parsed.flags["--file"] ? import_node_path13.default.resolve(cwd, parsed.flags["--file"]) : "";
15158
15716
  const useStdin = parsed.boolFlags.has("--stdin");
15159
15717
  const useJson = parsed.boolFlags.has("--json");
15160
15718
  const sourceCount = Number(Boolean(sourceFile)) + Number(useStdin);
@@ -15182,7 +15740,7 @@ function createRunUploadHandler(deps = {}) {
15182
15740
  } catch (error) {
15183
15741
  return {
15184
15742
  exitCode: ExitCode.InternalError,
15185
- stderr: [`ERROR: Failed to read run payload source: ${normalizeError5(error)}`]
15743
+ stderr: [`ERROR: Failed to read run payload source: ${normalizeError6(error)}`]
15186
15744
  };
15187
15745
  }
15188
15746
  const payloadFromSource = parseRunPayload(raw);
@@ -15212,7 +15770,7 @@ function createRunUploadHandler(deps = {}) {
15212
15770
  } catch (error) {
15213
15771
  return {
15214
15772
  exitCode: ExitCode.TransportError,
15215
- stderr: [`ERROR: ${normalizeError5(error)}`]
15773
+ stderr: [`ERROR: ${normalizeError6(error)}`]
15216
15774
  };
15217
15775
  }
15218
15776
  }
@@ -15292,7 +15850,7 @@ function createRunUploadHandler(deps = {}) {
15292
15850
  featureName: runInput.featureName,
15293
15851
  scenarioName: runInput.scenarioName,
15294
15852
  executedAt: runInput.executedAt,
15295
- errorMessage: normalizeError5(error)
15853
+ errorMessage: normalizeError6(error)
15296
15854
  })
15297
15855
  };
15298
15856
  }
@@ -15304,7 +15862,7 @@ function createRunUploadHandler(deps = {}) {
15304
15862
  `Scenario: ${runInput.scenarioName}`,
15305
15863
  `ExecutedAt: ${runInput.executedAt}`
15306
15864
  ],
15307
- stderr: [`ERROR: ${normalizeError5(error)}`]
15865
+ stderr: [`ERROR: ${normalizeError6(error)}`]
15308
15866
  };
15309
15867
  }
15310
15868
  };
@@ -15320,7 +15878,7 @@ var CONFIG_FLAGS8 = /* @__PURE__ */ new Set([
15320
15878
  "--jira-email",
15321
15879
  "--jira-api-token"
15322
15880
  ]);
15323
- function parseArgs8(args) {
15881
+ function parseArgs9(args) {
15324
15882
  const flags = {};
15325
15883
  const boolFlags = /* @__PURE__ */ new Set();
15326
15884
  const unknownFlags = [];
@@ -15358,7 +15916,7 @@ function pickConfigArgs8(parsed) {
15358
15916
  }
15359
15917
  return args;
15360
15918
  }
15361
- function normalizeError6(error) {
15919
+ function normalizeError7(error) {
15362
15920
  if (error instanceof ForgeClientError) {
15363
15921
  return `${error.code}: ${error.message}`;
15364
15922
  }
@@ -15422,7 +15980,7 @@ function createRunsHandler(deps = {}) {
15422
15980
  const env = deps.env ?? process.env;
15423
15981
  return async (request, context) => {
15424
15982
  const [subcommand, ...restArgs] = request.args;
15425
- const parsed = parseArgs8(restArgs);
15983
+ const parsed = parseArgs9(restArgs);
15426
15984
  const useJson = parsed.boolFlags.has("--json");
15427
15985
  const config = resolveCliConfig(pickConfigArgs8(parsed), env, cwd);
15428
15986
  const projectKey = config.values.projectKey;
@@ -15476,7 +16034,7 @@ function createRunsHandler(deps = {}) {
15476
16034
  } catch (error) {
15477
16035
  return {
15478
16036
  exitCode: ExitCode.TransportError,
15479
- stderr: [`ERROR: ${normalizeError6(error)}`]
16037
+ stderr: [`ERROR: ${normalizeError7(error)}`]
15480
16038
  };
15481
16039
  }
15482
16040
  }
@@ -15487,175 +16045,1655 @@ function createRunsHandler(deps = {}) {
15487
16045
  };
15488
16046
  }
15489
16047
 
15490
- // src/suites.ts
15491
- var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
15492
- "--config",
15493
- "--base-url",
15494
- "--project-key",
15495
- "--issue-key",
15496
- "--auth-mode",
15497
- "--jira-email",
15498
- "--jira-api-token"
15499
- ]);
15500
- function parseArgs9(args) {
15501
- const flags = {};
15502
- const boolFlags = /* @__PURE__ */ new Set();
15503
- const unknownFlags = [];
15504
- const supportedValueFlags = /* @__PURE__ */ new Set(["--key", ...CONFIG_FLAGS9]);
15505
- const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
15506
- for (let index = 0; index < args.length; index += 1) {
15507
- const token = args[index];
15508
- if (!token.startsWith("--")) {
15509
- continue;
15510
- }
15511
- if (supportedBoolFlags.has(token)) {
15512
- boolFlags.add(token);
15513
- continue;
15514
- }
15515
- if (!supportedValueFlags.has(token)) {
15516
- unknownFlags.push(token);
15517
- continue;
15518
- }
15519
- const value = args[index + 1];
15520
- if (!value || value.startsWith("--")) {
15521
- unknownFlags.push(token);
15522
- continue;
15523
- }
15524
- flags[token] = value;
15525
- index += 1;
15526
- }
15527
- return { flags, boolFlags, unknownFlags };
15528
- }
15529
- function pickConfigArgs9(parsed) {
15530
- const args = [];
15531
- for (const [flag, value] of Object.entries(parsed.flags)) {
15532
- if (CONFIG_FLAGS9.has(flag)) {
15533
- args.push(flag, value);
15534
- }
16048
+ // src/setup.ts
16049
+ var import_node_fs14 = require("node:fs");
16050
+ var import_node_path15 = __toESM(require("node:path"), 1);
16051
+
16052
+ // src/githubSetupProvider.ts
16053
+ var import_node_child_process4 = require("node:child_process");
16054
+
16055
+ // src/setupPlan.ts
16056
+ var import_node_crypto = require("node:crypto");
16057
+ var import_node_fs13 = require("node:fs");
16058
+ var import_node_path14 = __toESM(require("node:path"), 1);
16059
+ var SETUP_PLAN_SCHEMA_VERSION = "automatify.testops.setup/v1";
16060
+ var SETUP_PLAN_KIND = "AutomatifyTestOpsSetupPlan";
16061
+ var ACTIONS = [
16062
+ {
16063
+ id: "write-workflow-file",
16064
+ scope: "workflow-file",
16065
+ summary: "Write the approved GitHub Actions workflow into the local repository checkout.",
16066
+ mutates: "local-file"
16067
+ },
16068
+ {
16069
+ id: "set-callback-secrets",
16070
+ scope: "github-secrets",
16071
+ summary: "Create missing GitHub Actions callback secrets without exposing their values.",
16072
+ mutates: "github"
16073
+ },
16074
+ {
16075
+ id: "upsert-jira-profile",
16076
+ scope: "jira-profile",
16077
+ summary: "Create or update the Jira scenario automation profile through Forge contracts.",
16078
+ mutates: "jira"
15535
16079
  }
15536
- return args;
16080
+ ];
16081
+ function sha256(value) {
16082
+ return (0, import_node_crypto.createHash)("sha256").update(value, "utf8").digest("hex");
15537
16083
  }
15538
- function normalizeError7(error) {
15539
- if (error instanceof ForgeClientError) {
15540
- return `${error.code}: ${error.message}`;
16084
+ function sortJsonValue(value) {
16085
+ if (Array.isArray(value)) {
16086
+ return value.map(sortJsonValue);
15541
16087
  }
15542
- if (error instanceof Error) {
15543
- return error.message;
16088
+ if (value && typeof value === "object") {
16089
+ return Object.fromEntries(
16090
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortJsonValue(nested)])
16091
+ );
15544
16092
  }
15545
- return "Unknown suites command error.";
16093
+ return value;
15546
16094
  }
15547
- function toJsonSuite(suite) {
15548
- return {
15549
- id: suite.id,
15550
- key: suite.key,
15551
- name: suite.name,
15552
- description: suite.description ?? null,
15553
- linkedIssueKeys: [...suite.linkedIssueKeys ?? []],
15554
- createdAt: suite.createdAt,
15555
- updatedAt: suite.updatedAt
15556
- };
16095
+ function stableJson(value) {
16096
+ return JSON.stringify(sortJsonValue(value));
15557
16097
  }
15558
- function toJsonCase2(testCase) {
15559
- return {
15560
- id: testCase.id,
15561
- key: testCase.key,
15562
- title: testCase.title,
15563
- status: testCase.status,
15564
- description: testCase.description ?? null,
15565
- linkedIssueKeys: [...testCase.linkedIssueKeys ?? []],
15566
- createdAt: testCase.createdAt,
15567
- updatedAt: testCase.updatedAt
15568
- };
16098
+ function computePlanId(planWithoutId) {
16099
+ return `sha256:${sha256(stableJson(planWithoutId))}`;
15569
16100
  }
15570
- function suitesToLines(items) {
15571
- if (items.length === 0) {
15572
- return ["Test suites: 0", "No test suites found for the selected context."];
16101
+ function withoutPlanId(plan) {
16102
+ const { planId: _planId, ...rest } = plan;
16103
+ return rest;
16104
+ }
16105
+ function requireNonEmpty(value, field) {
16106
+ const normalized = value.trim();
16107
+ if (!normalized) {
16108
+ throw new Error(`${field} is required.`);
15573
16109
  }
15574
- return [
15575
- `Test suites: ${items.length}`,
15576
- ...items.map((item) => `- ${item.key} ${item.name}`)
15577
- ];
16110
+ return normalized;
15578
16111
  }
15579
- function suiteToLines(suite) {
15580
- return [
15581
- `Test suite: ${suite.key}`,
15582
- `Name: ${suite.name}`,
15583
- `Description: ${suite.description ?? "none"}`,
15584
- `Linked issues: ${(suite.linkedIssueKeys ?? []).join(", ") || "none"}`,
15585
- `Created: ${suite.createdAt}`,
15586
- `Updated: ${suite.updatedAt}`
15587
- ];
16112
+ function normalizeProjectKey(value) {
16113
+ const normalized = requireNonEmpty(value, "project key").toUpperCase();
16114
+ if (!/^[A-Z][A-Z0-9_]{1,31}$/.test(normalized)) {
16115
+ throw new Error("project key must contain 2-32 uppercase letters, digits, or underscores and start with a letter.");
16116
+ }
16117
+ return normalized;
15588
16118
  }
15589
- function suiteCasesToLines(suite, items) {
15590
- if (items.length === 0) {
15591
- return [
15592
- `Suite test cases: 0`,
15593
- `Suite: ${suite.key} ${suite.name}`,
15594
- "No test cases are currently linked to this suite."
15595
- ];
16119
+ function normalizeRepoPart(value, field) {
16120
+ const normalized = requireNonEmpty(value, field);
16121
+ if (!/^[A-Za-z0-9_.-]+$/.test(normalized)) {
16122
+ throw new Error(`${field} contains unsupported characters.`);
15596
16123
  }
15597
- return [
15598
- `Suite test cases: ${items.length}`,
15599
- `Suite: ${suite.key} ${suite.name}`,
15600
- ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
15601
- ];
16124
+ return normalized;
15602
16125
  }
15603
- function missingProjectResponse4() {
15604
- return {
15605
- exitCode: ExitCode.ValidationError,
15606
- stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
15607
- };
16126
+ function normalizeWorkflowPath(value) {
16127
+ const normalized = import_node_path14.default.posix.normalize(requireNonEmpty(value, "workflow path").replaceAll("\\", "/"));
16128
+ if (import_node_path14.default.posix.isAbsolute(normalized) || normalized.startsWith("../") || !normalized.startsWith(".github/workflows/") || !/\.ya?ml$/i.test(normalized)) {
16129
+ throw new Error("workflow path must be a relative .github/workflows/*.yml or *.yaml path without '..'.");
16130
+ }
16131
+ return normalized;
15608
16132
  }
15609
- function missingKeyResponse2(commandPath) {
16133
+ function normalizeSecretName(value, field) {
16134
+ const normalized = requireNonEmpty(value, field).toUpperCase();
16135
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(normalized)) {
16136
+ throw new Error(`${field} must be a valid GitHub Actions secret name.`);
16137
+ }
16138
+ return normalized;
16139
+ }
16140
+ function buildGitHubSetupPlan(input) {
16141
+ const projectKey = normalizeProjectKey(input.projectKey);
16142
+ const owner = normalizeRepoPart(input.owner, "GitHub owner");
16143
+ const repository = normalizeRepoPart(input.repository, "GitHub repository");
16144
+ const ref = requireNonEmpty(input.ref, "Git ref");
16145
+ const workflowId = requireNonEmpty(input.workflowId, "workflow id");
16146
+ const workflowPath = normalizeWorkflowPath(input.workflowPath);
16147
+ 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
+ );
16156
+ if (endpointSecretName === tokenSecretName) {
16157
+ throw new Error("callback endpoint and auth token secret names must be different.");
16158
+ }
16159
+ const workflowContent = (0, import_node_fs13.readFileSync)(input.workflowSourcePath, "utf8");
16160
+ if (!workflowContent.trim()) {
16161
+ throw new Error("workflow source file is empty.");
16162
+ }
16163
+ if (!/\bworkflow_dispatch\s*:/m.test(workflowContent)) {
16164
+ throw new Error("workflow source must declare workflow_dispatch so Forge can address it explicitly.");
16165
+ }
16166
+ for (const secretName of [endpointSecretName, tokenSecretName]) {
16167
+ if (!workflowContent.includes(`secrets.${secretName}`)) {
16168
+ throw new Error(`workflow source must reference GitHub Actions secret secrets.${secretName}.`);
16169
+ }
16170
+ }
16171
+ const actions = input.setProjectDefault ? [
16172
+ ...ACTIONS,
16173
+ {
16174
+ id: "set-project-default",
16175
+ scope: "project-default",
16176
+ summary: "Select the approved Jira automation profile as the project default through Forge contracts.",
16177
+ mutates: "jira"
16178
+ }
16179
+ ] : [...ACTIONS];
16180
+ const planWithoutId = {
16181
+ schemaVersion: SETUP_PLAN_SCHEMA_VERSION,
16182
+ kind: SETUP_PLAN_KIND,
16183
+ provider: "github-actions",
16184
+ project: { key: projectKey },
16185
+ github: {
16186
+ owner,
16187
+ repository,
16188
+ ref,
16189
+ workflowId,
16190
+ workflowFile: {
16191
+ path: workflowPath,
16192
+ sourcePath: import_node_path14.default.resolve(input.workflowSourcePath),
16193
+ sha256: sha256(workflowContent)
16194
+ },
16195
+ requiredSecrets: [
16196
+ {
16197
+ repositorySecretName: endpointSecretName,
16198
+ valueKey: "callbackEndpoint",
16199
+ purpose: "Authenticated Forge callback endpoint used by the workflow."
16200
+ },
16201
+ {
16202
+ repositorySecretName: tokenSecretName,
16203
+ valueKey: "callbackAuthToken",
16204
+ purpose: "Project-admin CLI key used by the workflow for Forge callbacks."
16205
+ }
16206
+ ],
16207
+ guidance: {
16208
+ authentication: "Prefer a GitHub App installation token or a fine-grained token with only the repository permissions required for workflow inspection, secrets administration, and workflow dispatch from Forge.",
16209
+ permissions: [
16210
+ "Actions: read (doctor) and write only when the Forge profile must dispatch workflow runs.",
16211
+ "Secrets: read/write metadata only for the explicit github-secrets apply scope.",
16212
+ "Contents: read for doctor; this command never creates commits or pull requests."
16213
+ ],
16214
+ remoteChangesExcluded: [
16215
+ "accounts",
16216
+ "credentials",
16217
+ "commits",
16218
+ "pull requests",
16219
+ "workflow runs"
16220
+ ]
16221
+ }
16222
+ },
16223
+ jiraProfile: {
16224
+ label: profileLabel,
16225
+ enabled: input.enabled,
16226
+ provider: "githubActions",
16227
+ config: {
16228
+ owner,
16229
+ repo: repository,
16230
+ workflowId,
16231
+ ref
16232
+ },
16233
+ setProjectDefault: input.setProjectDefault
16234
+ },
16235
+ actions,
16236
+ smokeValidation: [
16237
+ {
16238
+ id: "workflow-content",
16239
+ verifies: "Local and remote workflow content/path without dispatching it.",
16240
+ triggersExternalRun: false
16241
+ },
16242
+ {
16243
+ id: "callback-secret-metadata",
16244
+ verifies: "Required GitHub repository secret names exist; secret values remain unreadable.",
16245
+ triggersExternalRun: false
16246
+ },
16247
+ {
16248
+ id: "jira-profile",
16249
+ verifies: "The Forge automation profile and optional project default match the plan.",
16250
+ triggersExternalRun: false
16251
+ }
16252
+ ],
16253
+ rollback: [
16254
+ {
16255
+ actionId: "write-workflow-file",
16256
+ strategy: "Restore the previous file through version control or delete the newly created uncommitted file.",
16257
+ automatic: false
16258
+ },
16259
+ {
16260
+ actionId: "set-callback-secrets",
16261
+ strategy: "Rotate or delete the named GitHub repository secrets manually; previous secret values cannot be read or restored.",
16262
+ automatic: false
16263
+ },
16264
+ {
16265
+ actionId: "upsert-jira-profile",
16266
+ strategy: "Restore the pre-apply profile fields captured in the apply report, or delete a newly created unused profile through the existing Forge contract.",
16267
+ automatic: false
16268
+ },
16269
+ ...input.setProjectDefault ? [
16270
+ {
16271
+ actionId: "set-project-default",
16272
+ strategy: "Restore the pre-apply default profile id captured in the apply report.",
16273
+ automatic: false
16274
+ }
16275
+ ] : []
16276
+ ],
16277
+ followUp: {
16278
+ azureDevOps: "Add an Azure DevOps provider adapter over this schema/action boundary, mapping pipeline variables and the existing azureDevops Forge profile config without changing setup apply orchestration."
16279
+ }
16280
+ };
15610
16281
  return {
15611
- exitCode: ExitCode.ValidationError,
15612
- stderr: [`ERROR: Missing required --key for ${commandPath}.`]
16282
+ ...planWithoutId,
16283
+ planId: computePlanId(planWithoutId)
15613
16284
  };
15614
16285
  }
15615
- function createSuitesHandler(deps = {}) {
15616
- const cwd = deps.cwd ?? process.cwd();
15617
- const env = deps.env ?? process.env;
15618
- return async (request, context) => {
15619
- const [subcommand, ...restArgs] = request.args;
15620
- const parsed = parseArgs9(restArgs);
15621
- const useJson = parsed.boolFlags.has("--json");
15622
- const config = resolveCliConfig(pickConfigArgs9(parsed), env, cwd);
15623
- const projectKey = config.values.projectKey;
15624
- const issueKey = config.values.issueKey;
15625
- if (parsed.unknownFlags.length > 0) {
15626
- return {
15627
- exitCode: ExitCode.UsageError,
15628
- stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
15629
- };
16286
+ function isRecord(value) {
16287
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16288
+ }
16289
+ function hasExactKeys(value, keys) {
16290
+ const actual = Object.keys(value).sort();
16291
+ const expected = [...keys].sort();
16292
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
16293
+ }
16294
+ function isNonEmptyString(value) {
16295
+ return typeof value === "string" && value.trim().length > 0;
16296
+ }
16297
+ function validateRequiredSecrets(value, errors) {
16298
+ if (!Array.isArray(value) || value.length !== 2) {
16299
+ errors.push("github.requiredSecrets must contain exactly the callback endpoint and callback auth token metadata.");
16300
+ return false;
16301
+ }
16302
+ const keys = /* @__PURE__ */ new Set();
16303
+ const repositoryNames = /* @__PURE__ */ new Set();
16304
+ for (const [index, secret] of value.entries()) {
16305
+ if (!isRecord(secret) || !hasExactKeys(secret, ["repositorySecretName", "valueKey", "purpose"])) {
16306
+ errors.push(`github.requiredSecrets[${index}] has an invalid shape.`);
16307
+ continue;
15630
16308
  }
15631
- if (!projectKey) {
15632
- return missingProjectResponse4();
16309
+ if (!isNonEmptyString(secret.repositorySecretName) || !/^[A-Z_][A-Z0-9_]*$/.test(secret.repositorySecretName)) {
16310
+ errors.push(`github.requiredSecrets[${index}].repositorySecretName is invalid.`);
16311
+ } else {
16312
+ repositoryNames.add(secret.repositorySecretName);
15633
16313
  }
15634
- if (subcommand === "list") {
15635
- try {
15636
- const result = await context.invokeForgeContract("listTestSuites", {
15637
- context: {
15638
- projectKey,
15639
- issueKey: issueKey || void 0
15640
- }
15641
- });
15642
- if (!result.ok) {
15643
- return {
15644
- exitCode: ExitCode.RemoteError,
15645
- stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
15646
- };
15647
- }
15648
- if (useJson) {
15649
- return {
15650
- exitCode: ExitCode.Success,
15651
- stdout: toJsonLine({
15652
- action: "suites-list",
15653
- projectKey,
15654
- issueKey: issueKey || null,
15655
- count: result.data.length,
15656
- items: result.data.map((item) => toJsonSuite(item))
15657
- })
15658
- };
16314
+ if (secret.valueKey !== "callbackEndpoint" && secret.valueKey !== "callbackAuthToken") {
16315
+ errors.push(`github.requiredSecrets[${index}].valueKey is invalid.`);
16316
+ } else {
16317
+ keys.add(secret.valueKey);
16318
+ }
16319
+ if (!isNonEmptyString(secret.purpose)) {
16320
+ errors.push(`github.requiredSecrets[${index}].purpose is required.`);
16321
+ }
16322
+ }
16323
+ if (!keys.has("callbackEndpoint") || !keys.has("callbackAuthToken")) {
16324
+ errors.push("github.requiredSecrets must map both callbackEndpoint and callbackAuthToken.");
16325
+ }
16326
+ if (repositoryNames.size !== 2) {
16327
+ errors.push("github.requiredSecrets must use two distinct repository secret names.");
16328
+ }
16329
+ return errors.length === 0;
16330
+ }
16331
+ function validateActions(value, setDefault, errors) {
16332
+ const expected = setDefault ? [
16333
+ { id: "write-workflow-file", scope: "workflow-file", mutates: "local-file" },
16334
+ { id: "set-callback-secrets", scope: "github-secrets", mutates: "github" },
16335
+ { id: "upsert-jira-profile", scope: "jira-profile", mutates: "jira" },
16336
+ { id: "set-project-default", scope: "project-default", mutates: "jira" }
16337
+ ] : [
16338
+ { id: "write-workflow-file", scope: "workflow-file", mutates: "local-file" },
16339
+ { id: "set-callback-secrets", scope: "github-secrets", mutates: "github" },
16340
+ { id: "upsert-jira-profile", scope: "jira-profile", mutates: "jira" }
16341
+ ];
16342
+ if (!Array.isArray(value) || value.length !== expected.length) {
16343
+ errors.push("actions do not match the required setup mutation scopes.");
16344
+ return false;
16345
+ }
16346
+ value.forEach((action, index) => {
16347
+ if (!isRecord(action) || !hasExactKeys(action, ["id", "scope", "summary", "mutates"])) {
16348
+ errors.push(`actions[${index}] has an invalid shape.`);
16349
+ return;
16350
+ }
16351
+ const expectedAction = expected[index];
16352
+ if (action.id !== expectedAction.id || action.scope !== expectedAction.scope || action.mutates !== expectedAction.mutates) {
16353
+ errors.push(`actions[${index}] must be ${expectedAction.id}/${expectedAction.scope}/${expectedAction.mutates}.`);
16354
+ }
16355
+ if (!isNonEmptyString(action.id) || !isNonEmptyString(action.summary)) {
16356
+ errors.push(`actions[${index}] requires id and summary.`);
16357
+ }
16358
+ if (!["local-file", "github", "jira"].includes(action.mutates)) {
16359
+ errors.push(`actions[${index}].mutates is invalid.`);
16360
+ }
16361
+ });
16362
+ return errors.length === 0;
16363
+ }
16364
+ function validateGuidance(value, errors) {
16365
+ if (!isRecord(value) || !hasExactKeys(value, ["authentication", "permissions", "remoteChangesExcluded"])) {
16366
+ errors.push("github.guidance has an invalid shape.");
16367
+ return;
16368
+ }
16369
+ if (!isNonEmptyString(value.authentication)) {
16370
+ errors.push("github.guidance.authentication is required.");
16371
+ }
16372
+ if (!Array.isArray(value.permissions) || value.permissions.length === 0 || !value.permissions.every(isNonEmptyString)) {
16373
+ errors.push("github.guidance.permissions must be a non-empty string array.");
16374
+ }
16375
+ const expectedExclusions = ["accounts", "credentials", "commits", "pull requests", "workflow runs"];
16376
+ if (!Array.isArray(value.remoteChangesExcluded) || value.remoteChangesExcluded.length !== expectedExclusions.length || !value.remoteChangesExcluded.every((item, index) => item === expectedExclusions[index])) {
16377
+ errors.push("github.guidance.remoteChangesExcluded must be a string array.");
16378
+ }
16379
+ }
16380
+ function validateSmokeAndRollback(smoke, rollback, setDefault, errors) {
16381
+ const expectedSmokeIds = ["workflow-content", "callback-secret-metadata", "jira-profile"];
16382
+ if (!Array.isArray(smoke) || smoke.length !== expectedSmokeIds.length) {
16383
+ errors.push("smokeValidation must contain the three provider checks.");
16384
+ } else {
16385
+ smoke.forEach((check, index) => {
16386
+ if (!isRecord(check) || !hasExactKeys(check, ["id", "verifies", "triggersExternalRun"]) || check.id !== expectedSmokeIds[index] || !isNonEmptyString(check.verifies) || check.triggersExternalRun !== false) {
16387
+ errors.push(`smokeValidation[${index}] has an invalid shape or could trigger an external run.`);
16388
+ }
16389
+ });
16390
+ }
16391
+ const expectedRollbackIds = setDefault ? ["write-workflow-file", "set-callback-secrets", "upsert-jira-profile", "set-project-default"] : ["write-workflow-file", "set-callback-secrets", "upsert-jira-profile"];
16392
+ if (!Array.isArray(rollback) || rollback.length !== expectedRollbackIds.length) {
16393
+ errors.push("rollback must cover every mutation action.");
16394
+ } else {
16395
+ rollback.forEach((step, index) => {
16396
+ if (!isRecord(step) || !hasExactKeys(step, ["actionId", "strategy", "automatic"]) || step.actionId !== expectedRollbackIds[index] || !isNonEmptyString(step.strategy) || step.automatic !== false) {
16397
+ errors.push(`rollback[${index}] has an invalid shape or action id.`);
16398
+ }
16399
+ });
16400
+ }
16401
+ }
16402
+ function validateSetupPlan(value) {
16403
+ const errors = [];
16404
+ if (!isRecord(value)) {
16405
+ return { ok: false, errors: ["Plan must be a JSON object."] };
16406
+ }
16407
+ if (!hasExactKeys(value, [
16408
+ "schemaVersion",
16409
+ "kind",
16410
+ "planId",
16411
+ "provider",
16412
+ "project",
16413
+ "github",
16414
+ "jiraProfile",
16415
+ "actions",
16416
+ "smokeValidation",
16417
+ "rollback",
16418
+ "followUp"
16419
+ ])) {
16420
+ errors.push("Plan has missing or unsupported top-level fields.");
16421
+ }
16422
+ if (value.schemaVersion !== SETUP_PLAN_SCHEMA_VERSION) {
16423
+ errors.push(`schemaVersion must be ${SETUP_PLAN_SCHEMA_VERSION}.`);
16424
+ }
16425
+ if (value.kind !== SETUP_PLAN_KIND) {
16426
+ errors.push(`kind must be ${SETUP_PLAN_KIND}.`);
16427
+ }
16428
+ if (value.provider !== "github-actions") {
16429
+ errors.push("provider must be github-actions for this plan version.");
16430
+ }
16431
+ if (!isNonEmptyString(value.planId) || !value.planId.startsWith("sha256:")) {
16432
+ errors.push("planId must be a sha256 identifier.");
16433
+ }
16434
+ if (!isRecord(value.project) || !hasExactKeys(value.project, ["key"]) || !isNonEmptyString(value.project.key) || !/^[A-Z][A-Z0-9_]{1,31}$/.test(value.project.key)) {
16435
+ errors.push("project.key is required.");
16436
+ }
16437
+ 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
+ ])) {
16447
+ errors.push("github has an invalid shape.");
16448
+ } else {
16449
+ for (const field of ["owner", "repository", "ref", "workflowId"]) {
16450
+ if (!isNonEmptyString(github[field])) {
16451
+ errors.push(`github.${field} is required.`);
16452
+ }
16453
+ }
16454
+ if (isNonEmptyString(github.owner) && !/^[A-Za-z0-9_.-]+$/.test(github.owner)) {
16455
+ errors.push("github.owner contains unsupported characters.");
16456
+ }
16457
+ if (isNonEmptyString(github.repository) && !/^[A-Za-z0-9_.-]+$/.test(github.repository)) {
16458
+ errors.push("github.repository contains unsupported characters.");
16459
+ }
16460
+ if (!isRecord(github.workflowFile) || !hasExactKeys(github.workflowFile, ["path", "sourcePath", "sha256"])) {
16461
+ errors.push("github.workflowFile has an invalid shape.");
16462
+ } else {
16463
+ if (!isNonEmptyString(github.workflowFile.path)) {
16464
+ errors.push("github.workflowFile.path is required.");
16465
+ } else {
16466
+ try {
16467
+ normalizeWorkflowPath(github.workflowFile.path);
16468
+ } catch (error) {
16469
+ errors.push(error instanceof Error ? error.message : "github.workflowFile.path is invalid.");
16470
+ }
16471
+ }
16472
+ if (!isNonEmptyString(github.workflowFile.sourcePath)) {
16473
+ errors.push("github.workflowFile.sourcePath is required.");
16474
+ }
16475
+ if (typeof github.workflowFile.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(github.workflowFile.sha256)) {
16476
+ errors.push("github.workflowFile.sha256 must be a lowercase SHA-256 digest.");
16477
+ }
16478
+ }
16479
+ validateRequiredSecrets(github.requiredSecrets, errors);
16480
+ validateGuidance(github.guidance, errors);
16481
+ }
16482
+ const profile = value.jiraProfile;
16483
+ let setDefault = false;
16484
+ if (!isRecord(profile) || !hasExactKeys(profile, ["label", "enabled", "provider", "config", "setProjectDefault"])) {
16485
+ errors.push("jiraProfile has an invalid shape.");
16486
+ } else {
16487
+ setDefault = profile.setProjectDefault === true;
16488
+ if (!isNonEmptyString(profile.label)) {
16489
+ errors.push("jiraProfile.label is required.");
16490
+ }
16491
+ if (typeof profile.enabled !== "boolean" || typeof profile.setProjectDefault !== "boolean") {
16492
+ errors.push("jiraProfile enabled/default flags must be booleans.");
16493
+ }
16494
+ if (profile.provider !== "githubActions") {
16495
+ errors.push("jiraProfile.provider must be githubActions.");
16496
+ }
16497
+ if (!isRecord(profile.config) || !hasExactKeys(profile.config, ["owner", "repo", "workflowId", "ref"])) {
16498
+ errors.push("jiraProfile.config has an invalid shape.");
16499
+ } else if (isRecord(github)) {
16500
+ if (profile.config.owner !== github.owner || profile.config.repo !== github.repository || profile.config.workflowId !== github.workflowId || profile.config.ref !== github.ref) {
16501
+ errors.push("jiraProfile.config must match the GitHub repository/ref/workflow fields.");
16502
+ }
16503
+ }
16504
+ }
16505
+ validateActions(value.actions, setDefault, errors);
16506
+ validateSmokeAndRollback(value.smokeValidation, value.rollback, setDefault, errors);
16507
+ if (!isRecord(value.followUp) || !hasExactKeys(value.followUp, ["azureDevOps"]) || !isNonEmptyString(value.followUp.azureDevOps)) {
16508
+ errors.push("followUp.azureDevOps is required.");
16509
+ }
16510
+ if (errors.length === 0) {
16511
+ const plan = value;
16512
+ if (plan.planId !== computePlanId(withoutPlanId(plan))) {
16513
+ errors.push("planId does not match the plan contents.");
16514
+ }
16515
+ }
16516
+ return errors.length === 0 ? { ok: true, errors: [], plan: value } : { ok: false, errors };
16517
+ }
16518
+ function hashSetupContent(value) {
16519
+ return sha256(value);
16520
+ }
16521
+
16522
+ // src/githubSetupProvider.ts
16523
+ function isRecord2(value) {
16524
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16525
+ }
16526
+ function safeGitHubMessage(body, fallback) {
16527
+ if (isRecord2(body) && typeof body.message === "string") {
16528
+ return body.message.slice(0, 500);
16529
+ }
16530
+ return fallback;
16531
+ }
16532
+ async function githubApi(fetchImpl, url, token) {
16533
+ const response = await fetchImpl(url, {
16534
+ method: "GET",
16535
+ headers: {
16536
+ accept: "application/vnd.github+json",
16537
+ "x-github-api-version": "2022-11-28",
16538
+ ...token ? { authorization: `Bearer ${token}` } : {}
16539
+ }
16540
+ });
16541
+ const text = await response.text();
16542
+ let body = {};
16543
+ if (text) {
16544
+ try {
16545
+ body = JSON.parse(text);
16546
+ } catch {
16547
+ body = {};
16548
+ }
16549
+ }
16550
+ return { ok: response.ok, status: response.status, body };
16551
+ }
16552
+ async function setGitHubRepositorySecretWithCli(input) {
16553
+ 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
+ }
16564
+ }
16565
+ );
16566
+ let stderr = "";
16567
+ child.stderr.setEncoding("utf8");
16568
+ child.stderr.on("data", (chunk) => {
16569
+ stderr += chunk;
16570
+ });
16571
+ child.on("error", (error) => {
16572
+ reject(new Error(`Unable to start GitHub CLI: ${error.message}`));
16573
+ });
16574
+ child.on("close", (code) => {
16575
+ if (code === 0) {
16576
+ resolve();
16577
+ return;
16578
+ }
16579
+ const suffix = stderr.trim() ? ` ${stderr.trim().slice(0, 500)}` : "";
16580
+ reject(new Error(`GitHub CLI could not set repository secret ${input.name}.${suffix}`));
16581
+ });
16582
+ child.stdin.end(input.value, "utf8");
16583
+ });
16584
+ }
16585
+ var GitHubSetupProvider = class {
16586
+ name = "github-actions";
16587
+ fetchImpl;
16588
+ secretSetter;
16589
+ constructor(deps = {}) {
16590
+ this.fetchImpl = deps.fetchImpl ?? fetch;
16591
+ this.secretSetter = deps.setRepositorySecret ?? setGitHubRepositorySecretWithCli;
16592
+ }
16593
+ async inspectExecutionDefinition(plan, token) {
16594
+ const { owner, repository, workflowId, ref, workflowFile } = plan.github;
16595
+ const workflowUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/workflows/${encodeURIComponent(workflowId)}`;
16596
+ const workflow = await githubApi(this.fetchImpl, workflowUrl, token);
16597
+ if (!workflow.ok) {
16598
+ if (workflow.status === 404) {
16599
+ return {
16600
+ found: false,
16601
+ message: "Workflow is not registered on GitHub. Commit the local workflow to the repository default branch before expecting workflow_dispatch to be available."
16602
+ };
16603
+ }
16604
+ return {
16605
+ found: false,
16606
+ message: `GitHub workflow metadata could not be verified (HTTP ${workflow.status}: ${safeGitHubMessage(workflow.body, "request failed")}).`
16607
+ };
16608
+ }
16609
+ const workflowPath = isRecord2(workflow.body) && typeof workflow.body.path === "string" ? workflow.body.path.replace(/^\//, "") : void 0;
16610
+ const state = isRecord2(workflow.body) && typeof workflow.body.state === "string" ? workflow.body.state : void 0;
16611
+ const contentUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/contents/${workflowFile.path.split("/").map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`;
16612
+ const content = await githubApi(this.fetchImpl, contentUrl, token);
16613
+ let contentSha256;
16614
+ if (content.ok && isRecord2(content.body) && typeof content.body.content === "string") {
16615
+ try {
16616
+ const decoded = Buffer.from(content.body.content.replace(/\s/g, ""), "base64").toString("utf8");
16617
+ contentSha256 = hashSetupContent(decoded);
16618
+ } catch {
16619
+ contentSha256 = void 0;
16620
+ }
16621
+ }
16622
+ return {
16623
+ found: true,
16624
+ active: state === "active",
16625
+ path: workflowPath,
16626
+ contentSha256,
16627
+ 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."
16628
+ };
16629
+ }
16630
+ async inspectCallbackSecretMetadata(plan, adminToken) {
16631
+ if (!adminToken) {
16632
+ return {
16633
+ verified: false,
16634
+ names: /* @__PURE__ */ new Set(),
16635
+ message: "Repository secret metadata was not verified because githubAdminToken was not provided. Secret values are never readable."
16636
+ };
16637
+ }
16638
+ 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
+ }
16646
+ const names = /* @__PURE__ */ new Set();
16647
+ if (isRecord2(response.body) && Array.isArray(response.body.secrets)) {
16648
+ for (const item of response.body.secrets) {
16649
+ if (isRecord2(item) && typeof item.name === "string") {
16650
+ names.add(item.name);
16651
+ }
16652
+ }
16653
+ }
16654
+ return {
16655
+ verified: true,
16656
+ names,
16657
+ message: `Verified ${names.size} GitHub repository secret name(s); values remain unreadable.`
16658
+ };
16659
+ }
16660
+ async setCallbackSecret(plan, secret, value, adminToken) {
16661
+ await this.secretSetter({
16662
+ owner: plan.github.owner,
16663
+ repository: plan.github.repository,
16664
+ name: secret.repositorySecretName,
16665
+ value,
16666
+ adminToken
16667
+ });
16668
+ }
16669
+ };
16670
+
16671
+ // src/setup.ts
16672
+ var APPLY_RESULT_SCHEMA_VERSION = "automatify.testops.setup.apply/v1";
16673
+ var DOCTOR_RESULT_SCHEMA_VERSION = "automatify.testops.setup.doctor/v1";
16674
+ var SECRET_KEYS = [
16675
+ "githubProviderToken",
16676
+ "githubAdminToken",
16677
+ "callbackEndpoint",
16678
+ "callbackAuthToken"
16679
+ ];
16680
+ var MUTATION_SCOPES = [
16681
+ "workflow-file",
16682
+ "github-secrets",
16683
+ "jira-profile",
16684
+ "project-default"
16685
+ ];
16686
+ var SetupCommandError = class extends Error {
16687
+ cliCode;
16688
+ exitCode;
16689
+ constructor(cliCode, message, exitCode) {
16690
+ super(message);
16691
+ this.name = "SetupCommandError";
16692
+ this.cliCode = cliCode;
16693
+ this.exitCode = exitCode;
16694
+ }
16695
+ };
16696
+ var PLAN_VALUE_FLAGS = /* @__PURE__ */ new Set([
16697
+ "--provider",
16698
+ "--project-key",
16699
+ "--owner",
16700
+ "--repo",
16701
+ "--ref",
16702
+ "--workflow-id",
16703
+ "--workflow-path",
16704
+ "--workflow-source",
16705
+ "--profile-label",
16706
+ "--callback-endpoint-secret-name",
16707
+ "--callback-auth-token-secret-name"
16708
+ ]);
16709
+ var APPLY_VALUE_FLAGS = /* @__PURE__ */ new Set(["--plan", "--repo-root"]);
16710
+ var APPLY_BOOL_FLAGS = /* @__PURE__ */ new Set([
16711
+ "--confirm",
16712
+ "--dry-run",
16713
+ "--secrets-stdin",
16714
+ "--rotate-secrets",
16715
+ "--rotate-provider-token"
16716
+ ]);
16717
+ function defaultReadStdin() {
16718
+ return new Promise((resolve, reject) => {
16719
+ let value = "";
16720
+ process.stdin.setEncoding("utf8");
16721
+ process.stdin.on("data", (chunk) => {
16722
+ value += chunk;
16723
+ });
16724
+ process.stdin.on("end", () => resolve(value));
16725
+ process.stdin.on("error", reject);
16726
+ });
16727
+ }
16728
+ function parseArgs10(args, valueFlags, boolFlags, allowApplyCollections = false) {
16729
+ const flags = {};
16730
+ const enabled = /* @__PURE__ */ new Set();
16731
+ const approvals = [];
16732
+ const secretRefs = [];
16733
+ const errors = [];
16734
+ for (let index = 0; index < args.length; index += 1) {
16735
+ const token = args[index];
16736
+ if (!token.startsWith("--")) {
16737
+ errors.push(`Unexpected positional argument at index ${index}.`);
16738
+ continue;
16739
+ }
16740
+ if (boolFlags.has(token)) {
16741
+ enabled.add(token);
16742
+ continue;
16743
+ }
16744
+ if (allowApplyCollections && (token === "--approve" || token === "--secret-ref")) {
16745
+ const value2 = args[index + 1];
16746
+ if (!value2 || value2.startsWith("--")) {
16747
+ errors.push(`${token} requires a value.`);
16748
+ continue;
16749
+ }
16750
+ (token === "--approve" ? approvals : secretRefs).push(value2);
16751
+ index += 1;
16752
+ continue;
16753
+ }
16754
+ if (!valueFlags.has(token)) {
16755
+ errors.push(`Unknown option ${token}.`);
16756
+ continue;
16757
+ }
16758
+ const value = args[index + 1];
16759
+ if (!value || value.startsWith("--")) {
16760
+ errors.push(`${token} requires a value.`);
16761
+ continue;
16762
+ }
16763
+ flags[token] = value;
16764
+ index += 1;
16765
+ }
16766
+ return { flags, boolFlags: enabled, approvals, secretRefs, errors };
16767
+ }
16768
+ function jsonResponse(payload, exitCode = ExitCode.Success) {
16769
+ return {
16770
+ exitCode,
16771
+ stdout: [JSON.stringify(payload)]
16772
+ };
16773
+ }
16774
+ function errorPayload(code, message, diagnostics = []) {
16775
+ return {
16776
+ schemaVersion: APPLY_RESULT_SCHEMA_VERSION,
16777
+ status: "failed",
16778
+ error: {
16779
+ code,
16780
+ message,
16781
+ diagnostics
16782
+ }
16783
+ };
16784
+ }
16785
+ function safeErrorMessage(error, secrets = { values: {}, sources: {} }) {
16786
+ let message = error instanceof Error ? error.message : "Unknown setup error.";
16787
+ for (const secret of Object.values(secrets.values)) {
16788
+ if (secret) {
16789
+ message = message.split(secret).join("[REDACTED]");
16790
+ }
16791
+ }
16792
+ return message.replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [REDACTED]").replace(/token[=:]\s*[^\s,;]+/gi, "token=[REDACTED]");
16793
+ }
16794
+ function exitCodeForError(error) {
16795
+ if (error instanceof SetupCommandError) {
16796
+ return error.exitCode;
16797
+ }
16798
+ if (error instanceof ForgeClientError) {
16799
+ return error.code === "NETWORK_ERROR" || error.code === "TIMEOUT_ERROR" ? ExitCode.TransportError : ExitCode.RemoteError;
16800
+ }
16801
+ return ExitCode.RemoteError;
16802
+ }
16803
+ function errorCodeForError(error, fallback) {
16804
+ return error instanceof SetupCommandError ? error.cliCode : fallback;
16805
+ }
16806
+ function loadPlan(planPath, cwd) {
16807
+ const absolute = import_node_path15.default.resolve(cwd, planPath);
16808
+ let parsed;
16809
+ try {
16810
+ parsed = JSON.parse((0, import_node_fs14.readFileSync)(absolute, "utf8"));
16811
+ } catch (error) {
16812
+ throw new SetupCommandError(
16813
+ "PLAN_READ_ERROR",
16814
+ `Could not read setup plan JSON: ${error instanceof Error ? error.message : "unknown error"}`,
16815
+ ExitCode.ValidationError
16816
+ );
16817
+ }
16818
+ const validation = validateSetupPlan(parsed);
16819
+ if (!validation.ok || !validation.plan) {
16820
+ throw new SetupCommandError(
16821
+ "PLAN_VALIDATION_ERROR",
16822
+ `Setup plan validation failed: ${validation.errors.join(" ")}`,
16823
+ ExitCode.ValidationError
16824
+ );
16825
+ }
16826
+ return validation.plan;
16827
+ }
16828
+ function isSecretKey(value) {
16829
+ return SECRET_KEYS.includes(value);
16830
+ }
16831
+ async function resolveSecrets(parsed, deps) {
16832
+ const values = {};
16833
+ const sources = {};
16834
+ if (parsed.boolFlags.has("--secrets-stdin")) {
16835
+ const raw = await deps.readStdin();
16836
+ let envelope;
16837
+ try {
16838
+ envelope = JSON.parse(raw);
16839
+ } catch {
16840
+ throw new SetupCommandError(
16841
+ "SECRET_INPUT_ERROR",
16842
+ "Secret stdin must be a JSON object. Input content was not retained or printed.",
16843
+ ExitCode.ValidationError
16844
+ );
16845
+ }
16846
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
16847
+ throw new SetupCommandError("SECRET_INPUT_ERROR", "Secret stdin must be a JSON object.", ExitCode.ValidationError);
16848
+ }
16849
+ for (const [key, value] of Object.entries(envelope)) {
16850
+ if (!isSecretKey(key)) {
16851
+ throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin contains unsupported key ${key}.`, ExitCode.ValidationError);
16852
+ }
16853
+ 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);
16855
+ }
16856
+ values[key] = value.trim();
16857
+ sources[key] = "stdin";
16858
+ }
16859
+ }
16860
+ for (const reference of parsed.secretRefs) {
16861
+ const separator = reference.indexOf("=");
16862
+ const key = separator > 0 ? reference.slice(0, separator) : "";
16863
+ const target = separator > 0 ? reference.slice(separator + 1) : "";
16864
+ if (!isSecretKey(key) || !target.startsWith("env:")) {
16865
+ throw new SetupCommandError(
16866
+ "SECRET_REFERENCE_ERROR",
16867
+ "Secret references must use <supported-key>=env:<VARIABLE_NAME>.",
16868
+ ExitCode.ValidationError
16869
+ );
16870
+ }
16871
+ const envName = target.slice(4);
16872
+ 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);
16874
+ }
16875
+ if (sources[key]) {
16876
+ throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret ${key} was provided more than once.`, ExitCode.ValidationError);
16877
+ }
16878
+ const value = deps.env[envName]?.trim();
16879
+ if (!value) {
16880
+ throw new SetupCommandError(
16881
+ "SECRET_REFERENCE_ERROR",
16882
+ `Secret reference for ${key} points to an empty or missing environment variable.`,
16883
+ ExitCode.ValidationError
16884
+ );
16885
+ }
16886
+ values[key] = value;
16887
+ sources[key] = "env-ref";
16888
+ }
16889
+ return { values, sources };
16890
+ }
16891
+ function approvedScopes(plan, parsed, dryRun) {
16892
+ const planScopes = plan.actions.map((action) => action.scope);
16893
+ const requested = parsed.approvals.length === 0 && dryRun ? ["all"] : parsed.approvals;
16894
+ if (requested.length === 0) {
16895
+ throw new SetupCommandError(
16896
+ "APPROVAL_REQUIRED",
16897
+ "At least one --approve <scope> is required for apply. Use --approve all to approve every planned action.",
16898
+ ExitCode.UsageError
16899
+ );
16900
+ }
16901
+ const expanded = /* @__PURE__ */ new Set();
16902
+ for (const scope of requested) {
16903
+ if (scope === "all") {
16904
+ planScopes.forEach((item) => expanded.add(item));
16905
+ continue;
16906
+ }
16907
+ if (!MUTATION_SCOPES.includes(scope)) {
16908
+ throw new SetupCommandError("APPROVAL_ERROR", `Unsupported approval scope ${scope}.`, ExitCode.UsageError);
16909
+ }
16910
+ if (!planScopes.includes(scope)) {
16911
+ throw new SetupCommandError("APPROVAL_ERROR", `Approval scope ${scope} is not present in this plan.`, ExitCode.UsageError);
16912
+ }
16913
+ expanded.add(scope);
16914
+ }
16915
+ return planScopes.filter((scope) => expanded.has(scope));
16916
+ }
16917
+ function requireServiceData(result, contract) {
16918
+ if (!result.ok) {
16919
+ throw new Error(`${contract} failed: ${result.error.code}: ${result.error.message}`);
16920
+ }
16921
+ return result.data;
16922
+ }
16923
+ function findMatchingProfile(profiles, plan) {
16924
+ const matches = profiles.filter((profile) => profile.label === plan.jiraProfile.label);
16925
+ if (matches.length > 1) {
16926
+ throw new SetupCommandError(
16927
+ "AMBIGUOUS_PROFILE",
16928
+ `More than one Jira automation profile has label ${plan.jiraProfile.label}; rename duplicates before applying this deterministic plan.`,
16929
+ ExitCode.ValidationError
16930
+ );
16931
+ }
16932
+ return matches[0];
16933
+ }
16934
+ function profileMatches(profile, plan) {
16935
+ const config = profile.config;
16936
+ return profile.provider === "githubActions" && profile.label === plan.jiraProfile.label && profile.enabled === plan.jiraProfile.enabled && config.owner === plan.jiraProfile.config.owner && config.repo === plan.jiraProfile.config.repo && config.workflowId === plan.jiraProfile.config.workflowId && config.ref === plan.jiraProfile.config.ref;
16937
+ }
16938
+ function previousProfileState(profile) {
16939
+ if (!profile) {
16940
+ return { existed: false };
16941
+ }
16942
+ return {
16943
+ existed: true,
16944
+ id: profile.id,
16945
+ label: profile.label,
16946
+ provider: profile.provider,
16947
+ enabled: profile.enabled,
16948
+ wasProjectDefault: Boolean(profile.isProjectDefault),
16949
+ hadSecret: profile.hasSecret
16950
+ };
16951
+ }
16952
+ async function prepareState(plan, scopes, secrets, context, deps, dryRun, rotateSecrets, rotateProviderToken) {
16953
+ let profiles = [];
16954
+ let matchingProfile;
16955
+ let currentDefaultProfileId;
16956
+ if (scopes.includes("jira-profile") || scopes.includes("project-default")) {
16957
+ profiles = requireServiceData(
16958
+ await context.invokeForgeContract("listScenarioAutomationProfiles", {
16959
+ context: { projectKey: plan.project.key }
16960
+ }),
16961
+ "listScenarioAutomationProfiles"
16962
+ );
16963
+ matchingProfile = findMatchingProfile(profiles, plan);
16964
+ currentDefaultProfileId = profiles.find((profile) => profile.isProjectDefault)?.id;
16965
+ const needsProviderToken = scopes.includes("jira-profile") && (!matchingProfile?.hasSecret || rotateProviderToken);
16966
+ if (needsProviderToken && !dryRun && !secrets.values.githubProviderToken) {
16967
+ throw new SetupCommandError(
16968
+ "SECRET_INPUT_REQUIRED",
16969
+ "githubProviderToken is required to create this Jira profile, restore a missing provider secret, or rotate the provider token.",
16970
+ ExitCode.ValidationError
16971
+ );
16972
+ }
16973
+ }
16974
+ let repositorySecretNames;
16975
+ if (scopes.includes("github-secrets")) {
16976
+ if (!dryRun && !secrets.values.githubAdminToken) {
16977
+ throw new SetupCommandError(
16978
+ "SECRET_INPUT_REQUIRED",
16979
+ "githubAdminToken is required for the approved github-secrets scope.",
16980
+ ExitCode.ValidationError
16981
+ );
16982
+ }
16983
+ if (secrets.values.githubAdminToken) {
16984
+ const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(
16985
+ plan,
16986
+ secrets.values.githubAdminToken
16987
+ );
16988
+ repositorySecretNames = inspection.names;
16989
+ const secretsToWrite = plan.github.requiredSecrets.filter(
16990
+ (secret) => rotateSecrets || !repositorySecretNames?.has(secret.repositorySecretName)
16991
+ );
16992
+ if (!dryRun) {
16993
+ for (const secret of secretsToWrite) {
16994
+ if (!secrets.values[secret.valueKey]) {
16995
+ throw new SetupCommandError(
16996
+ "SECRET_INPUT_REQUIRED",
16997
+ `${secret.valueKey} is required to create or rotate ${secret.repositorySecretName}.`,
16998
+ ExitCode.ValidationError
16999
+ );
17000
+ }
17001
+ }
17002
+ }
17003
+ }
17004
+ }
17005
+ return { profiles, matchingProfile, currentDefaultProfileId, repositorySecretNames };
17006
+ }
17007
+ function rollbackFor(plan, actionId) {
17008
+ return plan.rollback.find((item) => item.actionId === actionId)?.strategy ?? "Review the plan rollback section.";
17009
+ }
17010
+ function resolveWorkflowTarget(plan, repoRoot) {
17011
+ const resolvedRoot = import_node_path15.default.resolve(repoRoot);
17012
+ const target = import_node_path15.default.resolve(resolvedRoot, plan.github.workflowFile.path);
17013
+ if (target !== resolvedRoot && !target.startsWith(`${resolvedRoot}${import_node_path15.default.sep}`)) {
17014
+ throw new SetupCommandError(
17015
+ "WORKFLOW_PATH_ERROR",
17016
+ "Workflow target escapes the approved repository root.",
17017
+ ExitCode.ValidationError
17018
+ );
17019
+ }
17020
+ return target;
17021
+ }
17022
+ function applyWorkflowFile(plan, repoRoot, dryRun) {
17023
+ const action = plan.actions.find((item) => item.scope === "workflow-file");
17024
+ const target = resolveWorkflowTarget(plan, repoRoot);
17025
+ const sourceContent = (0, import_node_fs14.readFileSync)(plan.github.workflowFile.sourcePath, "utf8");
17026
+ if (hashSetupContent(sourceContent) !== plan.github.workflowFile.sha256) {
17027
+ throw new SetupCommandError(
17028
+ "WORKFLOW_SOURCE_CHANGED",
17029
+ "Workflow source content no longer matches the approved plan hash; generate a new plan before applying.",
17030
+ ExitCode.ValidationError
17031
+ );
17032
+ }
17033
+ const existed = (0, import_node_fs14.existsSync)(target);
17034
+ const previousContent = existed ? (0, import_node_fs14.readFileSync)(target, "utf8") : void 0;
17035
+ const matches = previousContent !== void 0 && hashSetupContent(previousContent) === plan.github.workflowFile.sha256;
17036
+ if (matches) {
17037
+ return {
17038
+ id: action.id,
17039
+ scope: action.scope,
17040
+ status: "skipped",
17041
+ message: "Local workflow file already matches the approved content hash.",
17042
+ rollback: { available: false, guidance: "No workflow file mutation occurred." }
17043
+ };
17044
+ }
17045
+ if (!dryRun) {
17046
+ (0, import_node_fs14.mkdirSync)(import_node_path15.default.dirname(target), { recursive: true });
17047
+ (0, import_node_fs14.writeFileSync)(target, sourceContent, { encoding: "utf8", mode: 420 });
17048
+ }
17049
+ return {
17050
+ id: action.id,
17051
+ scope: action.scope,
17052
+ status: dryRun ? "planned" : existed ? "updated" : "created",
17053
+ message: dryRun ? `${existed ? "Update" : "Create"} ${plan.github.workflowFile.path} with sha256 ${plan.github.workflowFile.sha256}.` : `${existed ? "Updated" : "Created"} ${plan.github.workflowFile.path}.`,
17054
+ rollback: {
17055
+ available: !dryRun,
17056
+ guidance: rollbackFor(plan, action.id),
17057
+ previousState: {
17058
+ existed,
17059
+ sha256: previousContent === void 0 ? null : hashSetupContent(previousContent)
17060
+ }
17061
+ }
17062
+ };
17063
+ }
17064
+ async function applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets) {
17065
+ const action = plan.actions.find((item) => item.scope === "github-secrets");
17066
+ const names = state.repositorySecretNames;
17067
+ const pending = plan.github.requiredSecrets.filter(
17068
+ (secret) => rotateSecrets || !names?.has(secret.repositorySecretName)
17069
+ );
17070
+ if (names && pending.length === 0) {
17071
+ return {
17072
+ id: action.id,
17073
+ scope: action.scope,
17074
+ status: "skipped",
17075
+ message: "All required GitHub repository secret names already exist; values remain unreadable and were not overwritten.",
17076
+ rollback: { available: false, guidance: "No GitHub secret mutation occurred." }
17077
+ };
17078
+ }
17079
+ if (dryRun) {
17080
+ return {
17081
+ id: action.id,
17082
+ scope: action.scope,
17083
+ status: "planned",
17084
+ message: names ? `Set ${pending.map((secret) => secret.repositorySecretName).join(", ")}; values will be read only from approved secret input.` : "Inspect required GitHub secret metadata, then create missing names from approved secret input.",
17085
+ rollback: { available: false, guidance: rollbackFor(plan, action.id) }
17086
+ };
17087
+ }
17088
+ const adminToken = secrets.values.githubAdminToken;
17089
+ for (const secret of pending) {
17090
+ await deps.githubProvider.setCallbackSecret(
17091
+ plan,
17092
+ secret,
17093
+ secrets.values[secret.valueKey],
17094
+ adminToken
17095
+ );
17096
+ }
17097
+ return {
17098
+ id: action.id,
17099
+ scope: action.scope,
17100
+ status: "updated",
17101
+ message: `Set ${pending.length} GitHub repository secret(s). Values were not printed or placed in process arguments.`,
17102
+ rollback: { available: false, guidance: rollbackFor(plan, action.id) }
17103
+ };
17104
+ }
17105
+ async function applyJiraProfile(plan, state, secrets, context, dryRun, rotateProviderToken) {
17106
+ const action = plan.actions.find((item) => item.scope === "jira-profile");
17107
+ const current = state.matchingProfile;
17108
+ const matches = current ? profileMatches(current, plan) : false;
17109
+ if (current && matches && current.hasSecret && !rotateProviderToken) {
17110
+ return {
17111
+ profile: current,
17112
+ result: {
17113
+ id: action.id,
17114
+ scope: action.scope,
17115
+ status: "skipped",
17116
+ message: "Jira automation profile already matches the plan and has a stored provider secret.",
17117
+ rollback: { available: false, guidance: "No Jira profile mutation occurred." }
17118
+ }
17119
+ };
17120
+ }
17121
+ if (dryRun) {
17122
+ const plannedProfile = current ?? {
17123
+ id: "<created-by-forge>",
17124
+ projectKey: plan.project.key,
17125
+ label: plan.jiraProfile.label,
17126
+ provider: "githubActions",
17127
+ authType: "bearer",
17128
+ method: "POST",
17129
+ endpointSummary: "<computed-by-forge>",
17130
+ config: plan.jiraProfile.config,
17131
+ enabled: plan.jiraProfile.enabled,
17132
+ hasSecret: Boolean(secrets.values.githubProviderToken),
17133
+ createdAt: "",
17134
+ updatedAt: ""
17135
+ };
17136
+ return {
17137
+ profile: plannedProfile,
17138
+ result: {
17139
+ id: action.id,
17140
+ scope: action.scope,
17141
+ status: "planned",
17142
+ message: `${current ? "Update" : "Create"} the Jira automation profile through upsertScenarioAutomationProfile.`,
17143
+ rollback: {
17144
+ available: false,
17145
+ guidance: rollbackFor(plan, action.id),
17146
+ previousState: previousProfileState(current)
17147
+ }
17148
+ }
17149
+ };
17150
+ }
17151
+ const secretToken = !current?.hasSecret || rotateProviderToken ? secrets.values.githubProviderToken : void 0;
17152
+ const profile = requireServiceData(
17153
+ await context.invokeForgeContract("upsertScenarioAutomationProfile", {
17154
+ context: { projectKey: plan.project.key },
17155
+ input: {
17156
+ profileId: current?.id,
17157
+ label: plan.jiraProfile.label,
17158
+ provider: "githubActions",
17159
+ config: plan.jiraProfile.config,
17160
+ enabled: plan.jiraProfile.enabled,
17161
+ secretToken
17162
+ }
17163
+ }),
17164
+ "upsertScenarioAutomationProfile"
17165
+ );
17166
+ return {
17167
+ profile,
17168
+ result: {
17169
+ id: action.id,
17170
+ scope: action.scope,
17171
+ status: current ? "updated" : "created",
17172
+ message: `${current ? "Updated" : "Created"} Jira automation profile ${plan.jiraProfile.label} through the existing Forge contract.`,
17173
+ rollback: {
17174
+ available: true,
17175
+ guidance: rollbackFor(plan, action.id),
17176
+ previousState: previousProfileState(current)
17177
+ }
17178
+ }
17179
+ };
17180
+ }
17181
+ async function applyProjectDefault(plan, state, profile, context, dryRun) {
17182
+ const action = plan.actions.find((item) => item.scope === "project-default");
17183
+ if (state.currentDefaultProfileId === profile.id) {
17184
+ return {
17185
+ id: action.id,
17186
+ scope: action.scope,
17187
+ status: "skipped",
17188
+ message: "Jira automation profile is already the project default.",
17189
+ rollback: { available: false, guidance: "No project-default mutation occurred." }
17190
+ };
17191
+ }
17192
+ if (!dryRun) {
17193
+ requireServiceData(
17194
+ await context.invokeForgeContract("setScenarioAutomationDefaultProfile", {
17195
+ context: { projectKey: plan.project.key },
17196
+ profileId: profile.id
17197
+ }),
17198
+ "setScenarioAutomationDefaultProfile"
17199
+ );
17200
+ }
17201
+ return {
17202
+ id: action.id,
17203
+ scope: action.scope,
17204
+ status: dryRun ? "planned" : "updated",
17205
+ message: dryRun ? "Set the planned Jira automation profile as project default through setScenarioAutomationDefaultProfile." : "Set the Jira automation profile as project default through the existing Forge contract.",
17206
+ rollback: {
17207
+ available: !dryRun,
17208
+ guidance: rollbackFor(plan, action.id),
17209
+ previousState: { profileId: state.currentDefaultProfileId ?? null }
17210
+ }
17211
+ };
17212
+ }
17213
+ function doctorStatus(checks) {
17214
+ if (checks.some((check) => check.status === "fail")) return "fail";
17215
+ if (checks.some((check) => check.status === "warn")) return "warn";
17216
+ return "pass";
17217
+ }
17218
+ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
17219
+ const checks = [];
17220
+ let forgeTransportFailure = false;
17221
+ const target = resolveWorkflowTarget(plan, repoRoot);
17222
+ if (!(0, import_node_fs14.existsSync)(target)) {
17223
+ checks.push({
17224
+ id: "workflow-local",
17225
+ status: "fail",
17226
+ message: `Local workflow file ${plan.github.workflowFile.path} does not exist.`
17227
+ });
17228
+ } else {
17229
+ const localHash = hashSetupContent((0, import_node_fs14.readFileSync)(target, "utf8"));
17230
+ checks.push({
17231
+ id: "workflow-local",
17232
+ status: localHash === plan.github.workflowFile.sha256 ? "pass" : "fail",
17233
+ message: localHash === plan.github.workflowFile.sha256 ? "Local workflow content matches the approved plan hash." : "Local workflow content differs from the approved plan hash."
17234
+ });
17235
+ }
17236
+ try {
17237
+ const workflow = await deps.githubProvider.inspectExecutionDefinition(
17238
+ plan,
17239
+ secrets.values.githubProviderToken ?? secrets.values.githubAdminToken
17240
+ );
17241
+ const exact = workflow.found && workflow.active && workflow.path === plan.github.workflowFile.path && workflow.contentSha256 === plan.github.workflowFile.sha256;
17242
+ checks.push({
17243
+ id: "workflow-github",
17244
+ status: exact ? "pass" : workflow.found && (workflow.path !== plan.github.workflowFile.path || workflow.active === false) ? "fail" : "warn",
17245
+ message: workflow.message
17246
+ });
17247
+ } catch (error) {
17248
+ checks.push({
17249
+ id: "workflow-github",
17250
+ status: "warn",
17251
+ message: `GitHub workflow could not be verified: ${safeErrorMessage(error, secrets)}`
17252
+ });
17253
+ }
17254
+ try {
17255
+ const secretInspection = await deps.githubProvider.inspectCallbackSecretMetadata(
17256
+ plan,
17257
+ secrets.values.githubAdminToken
17258
+ );
17259
+ if (!secretInspection.verified) {
17260
+ checks.push({ id: "callback-secret-metadata", status: "warn", message: secretInspection.message });
17261
+ } else {
17262
+ const missing = plan.github.requiredSecrets.filter(
17263
+ (secret) => !secretInspection.names.has(secret.repositorySecretName)
17264
+ );
17265
+ checks.push({
17266
+ id: "callback-secret-metadata",
17267
+ status: missing.length === 0 ? "pass" : "fail",
17268
+ message: missing.length === 0 ? "All required GitHub repository secret names exist; values remain unreadable and were not printed." : `Missing GitHub repository secret name(s): ${missing.map((item) => item.repositorySecretName).join(", ")}.`
17269
+ });
17270
+ }
17271
+ } catch (error) {
17272
+ checks.push({
17273
+ id: "callback-secret-metadata",
17274
+ status: "warn",
17275
+ message: `GitHub repository secret metadata could not be verified: ${safeErrorMessage(error, secrets)}`
17276
+ });
17277
+ }
17278
+ try {
17279
+ const profiles = requireServiceData(
17280
+ await context.invokeForgeContract("listScenarioAutomationProfiles", {
17281
+ context: { projectKey: plan.project.key }
17282
+ }),
17283
+ "listScenarioAutomationProfiles"
17284
+ );
17285
+ const profile = findMatchingProfile(profiles, plan);
17286
+ const profileOk = Boolean(profile && profileMatches(profile, plan) && profile.hasSecret);
17287
+ checks.push({
17288
+ id: "jira-profile",
17289
+ status: profileOk ? "pass" : "fail",
17290
+ message: profileOk ? "Jira automation profile fields match the plan and Forge reports a stored provider secret." : "Jira automation profile is missing, differs from the plan, or lacks a provider secret."
17291
+ });
17292
+ if (plan.jiraProfile.setProjectDefault) {
17293
+ checks.push({
17294
+ id: "jira-project-default",
17295
+ status: profile?.isProjectDefault ? "pass" : "fail",
17296
+ message: profile?.isProjectDefault ? "Jira automation profile is the project default." : "Jira automation profile is not the project default."
17297
+ });
17298
+ }
17299
+ } catch (error) {
17300
+ forgeTransportFailure = error instanceof ForgeClientError && (error.code === "NETWORK_ERROR" || error.code === "TIMEOUT_ERROR");
17301
+ checks.push({
17302
+ id: "jira-profile",
17303
+ status: "fail",
17304
+ message: `Jira automation profile could not be verified: ${safeErrorMessage(error, secrets)}`
17305
+ });
17306
+ }
17307
+ const status = doctorStatus(checks);
17308
+ return {
17309
+ schemaVersion: DOCTOR_RESULT_SCHEMA_VERSION,
17310
+ planId: plan.planId,
17311
+ provider: "github-actions",
17312
+ status,
17313
+ exitCode: forgeTransportFailure ? ExitCode.TransportError : status === "fail" ? ExitCode.ValidationError : ExitCode.Success,
17314
+ externalRunTriggered: false,
17315
+ checks
17316
+ };
17317
+ }
17318
+ function planCommand(args, deps) {
17319
+ const parsed = parseArgs10(args, PLAN_VALUE_FLAGS, /* @__PURE__ */ new Set(["--set-default", "--disabled"]));
17320
+ if (parsed.errors.length > 0) {
17321
+ return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors), ExitCode.UsageError);
17322
+ }
17323
+ if ((parsed.flags["--provider"] ?? "github-actions") !== "github-actions") {
17324
+ return jsonResponse(
17325
+ errorPayload("VALIDATION_ERROR", "Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."),
17326
+ ExitCode.ValidationError
17327
+ );
17328
+ }
17329
+ try {
17330
+ const plan = buildGitHubSetupPlan({
17331
+ projectKey: parsed.flags["--project-key"] ?? "",
17332
+ owner: parsed.flags["--owner"] ?? "",
17333
+ repository: parsed.flags["--repo"] ?? "",
17334
+ ref: parsed.flags["--ref"] ?? "main",
17335
+ workflowId: parsed.flags["--workflow-id"] ?? "scenario-dispatch.yml",
17336
+ workflowPath: parsed.flags["--workflow-path"] ?? ".github/workflows/scenario-dispatch.yml",
17337
+ workflowSourcePath: import_node_path15.default.resolve(deps.cwd, parsed.flags["--workflow-source"] ?? ""),
17338
+ profileLabel: parsed.flags["--profile-label"] ?? "GitHub Actions",
17339
+ enabled: !parsed.boolFlags.has("--disabled"),
17340
+ setProjectDefault: parsed.boolFlags.has("--set-default"),
17341
+ callbackEndpointSecretName: parsed.flags["--callback-endpoint-secret-name"] ?? "TESTOPS_FORGE_ENDPOINT",
17342
+ callbackAuthTokenSecretName: parsed.flags["--callback-auth-token-secret-name"] ?? "TESTOPS_FORGE_AUTH_TOKEN"
17343
+ });
17344
+ return jsonResponse(plan);
17345
+ } catch (error) {
17346
+ return jsonResponse(
17347
+ errorPayload("VALIDATION_ERROR", safeErrorMessage(error)),
17348
+ ExitCode.ValidationError
17349
+ );
17350
+ }
17351
+ }
17352
+ async function applyCommand(args, context, deps) {
17353
+ const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, APPLY_BOOL_FLAGS, true);
17354
+ if (parsed.errors.length > 0) {
17355
+ return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors), ExitCode.UsageError);
17356
+ }
17357
+ const planPath = parsed.flags["--plan"];
17358
+ if (!planPath) {
17359
+ return jsonResponse(errorPayload("USAGE_ERROR", "--plan <file> is required."), ExitCode.UsageError);
17360
+ }
17361
+ const dryRun = parsed.boolFlags.has("--dry-run");
17362
+ if (!dryRun && !parsed.boolFlags.has("--confirm")) {
17363
+ return jsonResponse(
17364
+ errorPayload("CONFIRMATION_REQUIRED", "Mutating apply requires --confirm plus explicit --approve scopes."),
17365
+ ExitCode.UsageError
17366
+ );
17367
+ }
17368
+ let secrets = { values: {}, sources: {} };
17369
+ const actionResults = [];
17370
+ try {
17371
+ const plan = loadPlan(planPath, deps.cwd);
17372
+ const scopes = approvedScopes(plan, parsed, dryRun);
17373
+ secrets = await resolveSecrets(parsed, deps);
17374
+ const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
17375
+ const rotateSecrets = parsed.boolFlags.has("--rotate-secrets");
17376
+ 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
+ );
17387
+ let appliedProfile = state.matchingProfile;
17388
+ for (const action of plan.actions) {
17389
+ if (!scopes.includes(action.scope)) {
17390
+ actionResults.push({
17391
+ id: action.id,
17392
+ scope: action.scope,
17393
+ status: "skipped",
17394
+ message: "Action was not in the explicit approval scope.",
17395
+ rollback: { available: false, guidance: "No mutation occurred." }
17396
+ });
17397
+ continue;
17398
+ }
17399
+ try {
17400
+ if (action.scope === "workflow-file") {
17401
+ actionResults.push(applyWorkflowFile(plan, repoRoot, dryRun));
17402
+ } else if (action.scope === "github-secrets") {
17403
+ actionResults.push(await applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets));
17404
+ } else if (action.scope === "jira-profile") {
17405
+ const applied = await applyJiraProfile(
17406
+ plan,
17407
+ state,
17408
+ secrets,
17409
+ context,
17410
+ dryRun,
17411
+ rotateProviderToken
17412
+ );
17413
+ appliedProfile = applied.profile;
17414
+ actionResults.push(applied.result);
17415
+ } else if (action.scope === "project-default") {
17416
+ if (!appliedProfile) {
17417
+ throw new SetupCommandError(
17418
+ "PROFILE_REQUIRED",
17419
+ "Project-default apply requires an existing or approved Jira automation profile.",
17420
+ ExitCode.ValidationError
17421
+ );
17422
+ }
17423
+ actionResults.push(await applyProjectDefault(plan, state, appliedProfile, context, dryRun));
17424
+ }
17425
+ } catch (error) {
17426
+ actionResults.push({
17427
+ id: action.id,
17428
+ scope: action.scope,
17429
+ status: "failed",
17430
+ message: safeErrorMessage(error, secrets),
17431
+ rollback: {
17432
+ available: false,
17433
+ guidance: rollbackFor(plan, action.id)
17434
+ }
17435
+ });
17436
+ throw error;
17437
+ }
17438
+ }
17439
+ const doctor = dryRun ? void 0 : await inspectPlan(plan, repoRoot, secrets, context, deps);
17440
+ return jsonResponse(
17441
+ {
17442
+ schemaVersion: APPLY_RESULT_SCHEMA_VERSION,
17443
+ planId: plan.planId,
17444
+ provider: plan.provider,
17445
+ status: dryRun ? "dry-run" : doctor?.status === "fail" ? "applied-with-validation-failures" : doctor?.status === "warn" ? "applied-with-warnings" : "applied",
17446
+ dryRun,
17447
+ confirmed: !dryRun,
17448
+ approvedScopes: scopes,
17449
+ secretInputs: Object.entries(secrets.sources).map(([key, source]) => ({ key, source })),
17450
+ actions: actionResults,
17451
+ smokeValidation: doctor ?? {
17452
+ status: "not-run",
17453
+ externalRunTriggered: false,
17454
+ message: "Dry-run performs no mutations and does not require remote smoke validation. Run setup doctor for read-only verification."
17455
+ }
17456
+ },
17457
+ doctor?.exitCode ?? ExitCode.Success
17458
+ );
17459
+ } catch (error) {
17460
+ const message = safeErrorMessage(error, secrets);
17461
+ return jsonResponse(
17462
+ {
17463
+ ...errorPayload(errorCodeForError(error, "APPLY_FAILED"), message, [
17464
+ "Review each completed action and its rollback guidance; GitHub secret values cannot be restored automatically."
17465
+ ]),
17466
+ actions: actionResults
17467
+ },
17468
+ exitCodeForError(error)
17469
+ );
17470
+ }
17471
+ }
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
17478
+ );
17479
+ if (parsed.approvals.length > 0) {
17480
+ return jsonResponse(errorPayload("USAGE_ERROR", "setup doctor does not accept --approve."), ExitCode.UsageError);
17481
+ }
17482
+ if (parsed.errors.length > 0) {
17483
+ return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors), ExitCode.UsageError);
17484
+ }
17485
+ const planPath = parsed.flags["--plan"];
17486
+ if (!planPath) {
17487
+ return jsonResponse(errorPayload("USAGE_ERROR", "--plan <file> is required."), ExitCode.UsageError);
17488
+ }
17489
+ let secrets = { values: {}, sources: {} };
17490
+ try {
17491
+ const plan = loadPlan(planPath, deps.cwd);
17492
+ secrets = await resolveSecrets(parsed, deps);
17493
+ const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
17494
+ const summary = await inspectPlan(plan, repoRoot, secrets, context, deps);
17495
+ return jsonResponse(summary, summary.exitCode);
17496
+ } catch (error) {
17497
+ return jsonResponse(
17498
+ errorPayload(errorCodeForError(error, "DOCTOR_FAILED"), safeErrorMessage(error, secrets)),
17499
+ exitCodeForError(error)
17500
+ );
17501
+ }
17502
+ }
17503
+ function createSetupHandler(overrides = {}) {
17504
+ const deps = {
17505
+ cwd: overrides.cwd ?? process.cwd(),
17506
+ env: overrides.env ?? process.env,
17507
+ readStdin: overrides.readStdin ?? defaultReadStdin,
17508
+ githubProvider: overrides.githubProvider ?? new GitHubSetupProvider(overrides.githubProviderDeps)
17509
+ };
17510
+ return async (request, context) => {
17511
+ const [subcommand, ...args] = request.args;
17512
+ if (subcommand === "plan") {
17513
+ return planCommand(args, deps);
17514
+ }
17515
+ if (subcommand === "apply") {
17516
+ return applyCommand(args, context, deps);
17517
+ }
17518
+ if (subcommand === "doctor") {
17519
+ return doctorCommand(args, context, deps);
17520
+ }
17521
+ return jsonResponse(
17522
+ errorPayload("USAGE_ERROR", `Unsupported setup subcommand ${subcommand ?? "<missing>"}.`),
17523
+ ExitCode.UsageError
17524
+ );
17525
+ };
17526
+ }
17527
+
17528
+ // src/suites.ts
17529
+ var CONFIG_FLAGS9 = /* @__PURE__ */ new Set([
17530
+ "--config",
17531
+ "--base-url",
17532
+ "--project-key",
17533
+ "--issue-key",
17534
+ "--auth-mode",
17535
+ "--jira-email",
17536
+ "--jira-api-token"
17537
+ ]);
17538
+ function parseArgs11(args) {
17539
+ const flags = {};
17540
+ const boolFlags = /* @__PURE__ */ new Set();
17541
+ const unknownFlags = [];
17542
+ const supportedValueFlags = /* @__PURE__ */ new Set(["--key", ...CONFIG_FLAGS9]);
17543
+ const supportedBoolFlags = /* @__PURE__ */ new Set(["--json"]);
17544
+ for (let index = 0; index < args.length; index += 1) {
17545
+ const token = args[index];
17546
+ if (!token.startsWith("--")) {
17547
+ continue;
17548
+ }
17549
+ if (supportedBoolFlags.has(token)) {
17550
+ boolFlags.add(token);
17551
+ continue;
17552
+ }
17553
+ if (!supportedValueFlags.has(token)) {
17554
+ unknownFlags.push(token);
17555
+ continue;
17556
+ }
17557
+ const value = args[index + 1];
17558
+ if (!value || value.startsWith("--")) {
17559
+ unknownFlags.push(token);
17560
+ continue;
17561
+ }
17562
+ flags[token] = value;
17563
+ index += 1;
17564
+ }
17565
+ return { flags, boolFlags, unknownFlags };
17566
+ }
17567
+ function pickConfigArgs9(parsed) {
17568
+ const args = [];
17569
+ for (const [flag, value] of Object.entries(parsed.flags)) {
17570
+ if (CONFIG_FLAGS9.has(flag)) {
17571
+ args.push(flag, value);
17572
+ }
17573
+ }
17574
+ return args;
17575
+ }
17576
+ function normalizeError8(error) {
17577
+ if (error instanceof ForgeClientError) {
17578
+ return `${error.code}: ${error.message}`;
17579
+ }
17580
+ if (error instanceof Error) {
17581
+ return error.message;
17582
+ }
17583
+ return "Unknown suites command error.";
17584
+ }
17585
+ function toJsonSuite(suite) {
17586
+ return {
17587
+ id: suite.id,
17588
+ key: suite.key,
17589
+ name: suite.name,
17590
+ description: suite.description ?? null,
17591
+ linkedIssueKeys: [...suite.linkedIssueKeys ?? []],
17592
+ createdAt: suite.createdAt,
17593
+ updatedAt: suite.updatedAt
17594
+ };
17595
+ }
17596
+ function toJsonCase2(testCase) {
17597
+ return {
17598
+ id: testCase.id,
17599
+ key: testCase.key,
17600
+ title: testCase.title,
17601
+ status: testCase.status,
17602
+ description: testCase.description ?? null,
17603
+ linkedIssueKeys: [...testCase.linkedIssueKeys ?? []],
17604
+ createdAt: testCase.createdAt,
17605
+ updatedAt: testCase.updatedAt
17606
+ };
17607
+ }
17608
+ function suitesToLines(items) {
17609
+ if (items.length === 0) {
17610
+ return ["Test suites: 0", "No test suites found for the selected context."];
17611
+ }
17612
+ return [
17613
+ `Test suites: ${items.length}`,
17614
+ ...items.map((item) => `- ${item.key} ${item.name}`)
17615
+ ];
17616
+ }
17617
+ function suiteToLines(suite) {
17618
+ return [
17619
+ `Test suite: ${suite.key}`,
17620
+ `Name: ${suite.name}`,
17621
+ `Description: ${suite.description ?? "none"}`,
17622
+ `Linked issues: ${(suite.linkedIssueKeys ?? []).join(", ") || "none"}`,
17623
+ `Created: ${suite.createdAt}`,
17624
+ `Updated: ${suite.updatedAt}`
17625
+ ];
17626
+ }
17627
+ function suiteCasesToLines(suite, items) {
17628
+ if (items.length === 0) {
17629
+ return [
17630
+ `Suite test cases: 0`,
17631
+ `Suite: ${suite.key} ${suite.name}`,
17632
+ "No test cases are currently linked to this suite."
17633
+ ];
17634
+ }
17635
+ return [
17636
+ `Suite test cases: ${items.length}`,
17637
+ `Suite: ${suite.key} ${suite.name}`,
17638
+ ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
17639
+ ];
17640
+ }
17641
+ function missingProjectResponse4() {
17642
+ return {
17643
+ exitCode: ExitCode.ValidationError,
17644
+ stderr: ["ERROR: Missing project context. Set JIRA_PROJECT_KEY or pass --project-key."]
17645
+ };
17646
+ }
17647
+ function missingKeyResponse2(commandPath) {
17648
+ return {
17649
+ exitCode: ExitCode.ValidationError,
17650
+ stderr: [`ERROR: Missing required --key for ${commandPath}.`]
17651
+ };
17652
+ }
17653
+ function createSuitesHandler(deps = {}) {
17654
+ const cwd = deps.cwd ?? process.cwd();
17655
+ const env = deps.env ?? process.env;
17656
+ return async (request, context) => {
17657
+ const [subcommand, ...restArgs] = request.args;
17658
+ const parsed = parseArgs11(restArgs);
17659
+ const useJson = parsed.boolFlags.has("--json");
17660
+ const config = resolveCliConfig(pickConfigArgs9(parsed), env, cwd);
17661
+ const projectKey = config.values.projectKey;
17662
+ const issueKey = config.values.issueKey;
17663
+ if (parsed.unknownFlags.length > 0) {
17664
+ return {
17665
+ exitCode: ExitCode.UsageError,
17666
+ stderr: [`ERROR: Unknown or invalid flags: ${parsed.unknownFlags.join(", ")}`]
17667
+ };
17668
+ }
17669
+ if (!projectKey) {
17670
+ return missingProjectResponse4();
17671
+ }
17672
+ if (subcommand === "list") {
17673
+ try {
17674
+ const result = await context.invokeForgeContract("listTestSuites", {
17675
+ context: {
17676
+ projectKey,
17677
+ issueKey: issueKey || void 0
17678
+ }
17679
+ });
17680
+ if (!result.ok) {
17681
+ return {
17682
+ exitCode: ExitCode.RemoteError,
17683
+ stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
17684
+ };
17685
+ }
17686
+ if (useJson) {
17687
+ return {
17688
+ exitCode: ExitCode.Success,
17689
+ stdout: toJsonLine({
17690
+ action: "suites-list",
17691
+ projectKey,
17692
+ issueKey: issueKey || null,
17693
+ count: result.data.length,
17694
+ items: result.data.map((item) => toJsonSuite(item))
17695
+ })
17696
+ };
15659
17697
  }
15660
17698
  return {
15661
17699
  exitCode: ExitCode.Success,
@@ -15664,7 +17702,7 @@ function createSuitesHandler(deps = {}) {
15664
17702
  } catch (error) {
15665
17703
  return {
15666
17704
  exitCode: ExitCode.TransportError,
15667
- stderr: [`ERROR: ${normalizeError7(error)}`]
17705
+ stderr: [`ERROR: ${normalizeError8(error)}`]
15668
17706
  };
15669
17707
  }
15670
17708
  }
@@ -15705,7 +17743,7 @@ function createSuitesHandler(deps = {}) {
15705
17743
  } catch (error) {
15706
17744
  return {
15707
17745
  exitCode: ExitCode.TransportError,
15708
- stderr: [`ERROR: ${normalizeError7(error)}`]
17746
+ stderr: [`ERROR: ${normalizeError8(error)}`]
15709
17747
  };
15710
17748
  }
15711
17749
  }
@@ -15769,7 +17807,7 @@ function createSuitesHandler(deps = {}) {
15769
17807
  } catch (error) {
15770
17808
  return {
15771
17809
  exitCode: ExitCode.TransportError,
15772
- stderr: [`ERROR: ${normalizeError7(error)}`]
17810
+ stderr: [`ERROR: ${normalizeError8(error)}`]
15773
17811
  };
15774
17812
  }
15775
17813
  }
@@ -15791,7 +17829,7 @@ var CONFIG_FLAGS10 = /* @__PURE__ */ new Set([
15791
17829
  "--jira-api-token"
15792
17830
  ]);
15793
17831
  var RECONCILE_CONFIRM_TOKEN = "RECONCILE";
15794
- function parseArgs10(args) {
17832
+ function parseArgs12(args) {
15795
17833
  const flags = {};
15796
17834
  const boolFlags = /* @__PURE__ */ new Set();
15797
17835
  const unknownFlags = [];
@@ -15829,7 +17867,7 @@ function pickConfigArgs10(parsed) {
15829
17867
  }
15830
17868
  return args;
15831
17869
  }
15832
- function normalizeError8(error) {
17870
+ function normalizeError9(error) {
15833
17871
  if (error instanceof ForgeClientError) {
15834
17872
  return `${error.code}: ${error.message}`;
15835
17873
  }
@@ -15880,7 +17918,7 @@ function createSyncHandler(deps = {}) {
15880
17918
  const env = deps.env ?? process.env;
15881
17919
  return async (request, context) => {
15882
17920
  const [subcommand, ...restArgs] = request.args;
15883
- const parsed = parseArgs10(restArgs);
17921
+ const parsed = parseArgs12(restArgs);
15884
17922
  const useJson = parsed.boolFlags.has("--json");
15885
17923
  const config = resolveCliConfig(pickConfigArgs10(parsed), env, cwd);
15886
17924
  const projectKey = config.values.projectKey;
@@ -15932,7 +17970,7 @@ function createSyncHandler(deps = {}) {
15932
17970
  } catch (error) {
15933
17971
  return {
15934
17972
  exitCode: ExitCode.TransportError,
15935
- stderr: [`ERROR: ${normalizeError8(error)}`]
17973
+ stderr: [`ERROR: ${normalizeError9(error)}`]
15936
17974
  };
15937
17975
  }
15938
17976
  }
@@ -15978,7 +18016,7 @@ function createSyncHandler(deps = {}) {
15978
18016
  } catch (error) {
15979
18017
  return {
15980
18018
  exitCode: ExitCode.TransportError,
15981
- stderr: [`ERROR: ${normalizeError8(error)}`]
18019
+ stderr: [`ERROR: ${normalizeError9(error)}`]
15982
18020
  };
15983
18021
  }
15984
18022
  }
@@ -15993,8 +18031,8 @@ function createSyncHandler(deps = {}) {
15993
18031
  var COMMAND_REGISTRY = [
15994
18032
  {
15995
18033
  name: "allure",
15996
- description: "Allure evidence upload/download commands for Jira issues",
15997
- subcommands: ["upload", "download"],
18034
+ description: "Allure evidence upload/download/open commands for Jira issues",
18035
+ subcommands: ["upload", "download", "open"],
15998
18036
  handler: createAllureHandler()
15999
18037
  },
16000
18038
  {
@@ -16040,6 +18078,12 @@ var COMMAND_REGISTRY = [
16040
18078
  subcommands: ["show"],
16041
18079
  handler: createRunsHandler()
16042
18080
  },
18081
+ {
18082
+ name: "setup",
18083
+ description: "Plan, apply, and verify provider onboarding without duplicating Forge domain logic",
18084
+ subcommands: ["plan", "apply", "doctor"],
18085
+ handler: createSetupHandler()
18086
+ },
16043
18087
  {
16044
18088
  name: "suites",
16045
18089
  description: "Hierarchy-aware suite browse commands",
@@ -16049,7 +18093,7 @@ var COMMAND_REGISTRY = [
16049
18093
  {
16050
18094
  name: "doctor",
16051
18095
  description: "Preflight and health checks",
16052
- subcommands: ["check"],
18096
+ subcommands: ["check", "allure"],
16053
18097
  handler: createDoctorHandler()
16054
18098
  },
16055
18099
  {
@@ -16234,6 +18278,7 @@ function showTopLevelHelp() {
16234
18278
  "",
16235
18279
  "Available products:",
16236
18280
  " testops TestOps \u2014 test management, BDD, execution, reporting",
18281
+ " allure Allure evidence diagnostics and local report workflows (doctor, open)",
16237
18282
  "",
16238
18283
  "Boundary:",
16239
18284
  " Thin client over Forge contracts or Jira Cloud APIs.",
@@ -16243,6 +18288,29 @@ function showTopLevelHelp() {
16243
18288
  console.log(line);
16244
18289
  }
16245
18290
  }
18291
+ function showAllureHelp() {
18292
+ const lines = [
18293
+ "Automatify Allure",
18294
+ "",
18295
+ "Usage:",
18296
+ " automatify allure doctor [options]",
18297
+ " automatify allure open <zipPath> [options]",
18298
+ "",
18299
+ "Commands:",
18300
+ " doctor Check local Allure/Jira readiness",
18301
+ " open Extract a generated Allure HTML report ZIP and serve it locally",
18302
+ "",
18303
+ "Examples:",
18304
+ " automatify allure doctor",
18305
+ " automatify allure doctor --json",
18306
+ " automatify allure doctor --issue-key ABC-123",
18307
+ " automatify allure open ./allure-report.zip",
18308
+ " automatify allure open ./allure-report.zip --port 8080 --no-open"
18309
+ ];
18310
+ for (const line of lines) {
18311
+ console.log(line);
18312
+ }
18313
+ }
16246
18314
  async function main() {
16247
18315
  const args = process.argv.slice(2);
16248
18316
  const [productArg, ...restArgs] = args;
@@ -16251,6 +18319,28 @@ async function main() {
16251
18319
  process.exitCode = 0;
16252
18320
  return;
16253
18321
  }
18322
+ if (productArg === "allure") {
18323
+ const [commandArg, ...commandRest] = restArgs;
18324
+ if (!commandArg || commandArg === "--help" || commandArg === "-h" || commandArg === "help") {
18325
+ showAllureHelp();
18326
+ process.exitCode = 0;
18327
+ return;
18328
+ }
18329
+ if (commandArg === "doctor") {
18330
+ const exitCode2 = await runCli(["doctor", "allure", ...commandRest]);
18331
+ process.exitCode = exitCode2;
18332
+ return;
18333
+ }
18334
+ if (commandArg === "open") {
18335
+ const exitCode2 = await runCli(["allure", "open", ...commandRest]);
18336
+ process.exitCode = exitCode2;
18337
+ return;
18338
+ }
18339
+ console.error(`Unknown allure command: ${commandArg}`);
18340
+ console.error("Run `automatify allure --help` to see available commands.");
18341
+ process.exitCode = 1;
18342
+ return;
18343
+ }
16254
18344
  if (productArg !== "testops") {
16255
18345
  console.error(`Unknown product: ${productArg}`);
16256
18346
  console.error("Run `automatify --help` to see available products.");