repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
pi/agent/skillz/ims-process-review/scripts/atlassian-request.mjs
Raw#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { realpathSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
const MUTATIONS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
export function parseArgs(argv) {
const args = [...argv];
let method = "GET";
if (METHODS.has(args[0]?.toUpperCase())) method = args.shift().toUpperCase();
const options = {
method,
path: undefined,
bodyFile: undefined,
profile: undefined,
paginate: false,
maxPages: 100,
allowMutation: false,
};
while (args.length) {
const arg = args.shift();
switch (arg) {
case "--body":
options.bodyFile = requiredValue(arg, args.shift());
break;
case "--profile":
options.profile = requiredValue(arg, args.shift());
break;
case "--paginate":
options.paginate = true;
break;
case "--max-pages": {
const value = Number(requiredValue(arg, args.shift()));
if (!Number.isSafeInteger(value) || value < 1) throw new Error("--max-pages must be a positive integer");
options.maxPages = value;
break;
}
case "--allow-mutation":
options.allowMutation = true;
break;
case "--help":
options.help = true;
break;
default:
if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`);
if (options.path) throw new Error(`Unexpected argument: ${arg}`);
options.path = arg;
}
}
if (options.help) return options;
if (!options.path) throw new Error("Request path is required");
if (MUTATIONS.has(method) && !options.allowMutation) throw new Error(`${method} requires --allow-mutation`);
if (options.bodyFile && method === "GET") throw new Error("GET does not accept --body");
if (options.paginate && method !== "GET") throw new Error("--paginate requires GET");
return options;
}
function requiredValue(option, value) {
if (!value || value.startsWith("--")) throw new Error(`${option} requires a value`);
return value;
}
export function selectConnection(profiles, profileName, env = process.env) {
const selected = profileName
? profiles.find((profile) => profile.name === profileName)
: profiles.find((profile) => profile.is_default) ?? profiles[0];
if (profileName && !selected && (!env.ATLASSIAN_BASE_URL || !env.ATLASSIAN_EMAIL)) {
throw new Error(`Unknown Atlassian profile: ${profileName}`);
}
const baseUrl = env.ATLASSIAN_BASE_URL ?? selected?.base_url;
const email = env.ATLASSIAN_EMAIL ?? selected?.email;
if (!baseUrl || !email) throw new Error("Atlassian base URL and email are unavailable");
return { baseUrl, email };
}
export function resolveRequestUrl(path, baseUrl) {
const base = new URL(baseUrl);
if (base.protocol !== "https:" && !["localhost", "127.0.0.1", "::1"].includes(base.hostname)) {
throw new Error(`Refusing non-HTTPS Atlassian base URL: ${base.origin}`);
}
const url = new URL(path, base);
if (url.origin !== base.origin) throw new Error(`Refusing to send Atlassian credentials to ${url.origin}`);
return url;
}
export function nextPageUrl(body, currentUrl) {
const linked = body?._links?.next ?? body?.links?.next ?? body?.nextPage ?? (typeof body?.next === "string" ? body.next : undefined);
if (linked) return new URL(linked, currentUrl);
if (Number.isFinite(body?.startAt) && Number.isFinite(body?.maxResults) && Number.isFinite(body?.total)) {
const startAt = body.startAt + body.maxResults;
if (startAt < body.total) {
const next = new URL(currentUrl);
next.searchParams.set("startAt", String(startAt));
return next;
}
}
if (body?.nextPageToken && body?.isLast !== true) {
const next = new URL(currentUrl);
next.searchParams.set("nextPageToken", body.nextPageToken);
return next;
}
return undefined;
}
export async function requestJson(url, init, fetchImpl = fetch) {
const response = await fetchImpl(url, { ...init, redirect: "manual" });
const text = await response.text();
if (response.status >= 300 && response.status < 400) {
throw new Error(`${init.method} ${url}: redirect ${response.status} refused`);
}
if (!response.ok) throw new Error(`${init.method} ${url}: HTTP ${response.status}${text ? `\n${text}` : ""}`);
if (!text) return null;
try {
return JSON.parse(text);
} catch {
return text;
}
}
export async function runRequest(options, connection, token, fetchImpl = fetch) {
if (!token) throw new Error("ATLASSIAN_API_TOKEN or JIRA_API_TOKEN is required");
const firstUrl = resolveRequestUrl(options.path, connection.baseUrl);
const headers = {
Accept: "application/json",
Authorization: `Basic ${Buffer.from(`${connection.email}:${token}`).toString("base64")}`,
};
let body;
if (options.bodyFile) {
const input = await readFile(options.bodyFile, "utf8");
body = JSON.stringify(JSON.parse(input));
headers["Content-Type"] = "application/json";
}
const init = { method: options.method, headers, body };
if (!options.paginate) return requestJson(firstUrl, init, fetchImpl);
const pages = [];
let url = firstUrl;
for (let page = 1; ; page++) {
pages.push(await requestJson(url, init, fetchImpl));
const next = nextPageUrl(pages.at(-1), url);
if (!next) return pages;
if (page >= options.maxPages) throw new Error(`Pagination exceeded --max-pages ${options.maxPages}`);
url = resolveRequestUrl(next.href, connection.baseUrl);
}
}
function loadProfiles() {
const result = spawnSync("atlassian-cli", ["auth", "list", "--format", "json"], {
encoding: "utf8",
windowsHide: true,
});
if (result.status !== 0) throw new Error(result.stderr.trim() || "atlassian-cli auth list failed");
return JSON.parse(result.stdout);
}
function usage() {
return `Usage: node atlassian-request.mjs [METHOD] PATH [options]
METHOD defaults to GET. PATH must resolve to the configured Atlassian origin.
On Windows Bash, use relative paths without a leading slash.
Options:
--body FILE JSON request body
--profile NAME atlassian-cli profile
--paginate fetch all GET pages and output an array of responses
--max-pages NUMBER pagination limit, default 100
--allow-mutation required for POST, PUT, PATCH, and DELETE
--help show this help`;
}
async function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
console.log(usage());
return;
}
const profiles = process.env.ATLASSIAN_BASE_URL && process.env.ATLASSIAN_EMAIL ? [] : loadProfiles();
const connection = selectConnection(profiles, options.profile);
const token = process.env.ATLASSIAN_API_TOKEN ?? process.env.JIRA_API_TOKEN;
const result = await runRequest(options, connection, token);
console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
}
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
main().catch((error) => {
console.error(error.message);
process.exit(1);
});
}