266 lines
7.3 KiB
TypeScript
266 lines
7.3 KiB
TypeScript
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||
import { markdownToAdf } from "./adf.js";
|
||
import { sendJsonRequest } from "./http.js";
|
||
import type { AtlassianConfig, CommandOutput, FetchLike, JiraIssueSummary } from "./types.js";
|
||
|
||
const ISSUE_FIELDS = ["summary", "issuetype", "status", "assignee", "created", "updated"] as const;
|
||
|
||
type JiraClientOptions = {
|
||
config: AtlassianConfig;
|
||
fetchImpl?: FetchLike;
|
||
};
|
||
|
||
type SearchInput = {
|
||
jql: string;
|
||
maxResults: number;
|
||
startAt: number;
|
||
};
|
||
|
||
type CreateInput = {
|
||
project?: string;
|
||
type: string;
|
||
summary: string;
|
||
description?: string;
|
||
dryRun?: boolean;
|
||
};
|
||
|
||
type UpdateInput = {
|
||
issue: string;
|
||
summary?: string;
|
||
description?: string;
|
||
dryRun?: boolean;
|
||
};
|
||
|
||
type CommentInput = {
|
||
issue: string;
|
||
body: string;
|
||
dryRun?: boolean;
|
||
};
|
||
|
||
type TransitionInput = {
|
||
issue: string;
|
||
transition: string;
|
||
dryRun?: boolean;
|
||
};
|
||
|
||
function normalizeIssue(config: AtlassianConfig, issue: Record<string, unknown>): JiraIssueSummary {
|
||
const fields = (issue.fields ?? {}) as Record<string, unknown>;
|
||
const issueType = (fields.issuetype ?? {}) as Record<string, unknown>;
|
||
const status = (fields.status ?? {}) as Record<string, unknown>;
|
||
const assignee = (fields.assignee ?? {}) as Record<string, unknown>;
|
||
|
||
return {
|
||
key: String(issue.key ?? ""),
|
||
summary: String(fields.summary ?? ""),
|
||
issueType: String(issueType.name ?? ""),
|
||
status: String(status.name ?? ""),
|
||
assignee: assignee.displayName ? String(assignee.displayName) : undefined,
|
||
created: String(fields.created ?? ""),
|
||
updated: String(fields.updated ?? ""),
|
||
url: `${config.baseUrl}/browse/${issue.key ?? ""}`,
|
||
};
|
||
}
|
||
|
||
function createRequest(config: AtlassianConfig, method: "GET" | "POST" | "PUT", path: string, body?: unknown) {
|
||
const url = new URL(path, `${config.jiraBaseUrl}/`);
|
||
|
||
return {
|
||
method,
|
||
url: url.toString(),
|
||
...(body === undefined ? {} : { body }),
|
||
};
|
||
}
|
||
|
||
export function createJiraClient(options: JiraClientOptions) {
|
||
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
||
|
||
if (!fetchImpl) {
|
||
throw new Error("Fetch API is not available in this runtime");
|
||
}
|
||
|
||
async function send(method: "GET" | "POST" | "PUT", path: string, body?: unknown) {
|
||
const request = createRequest(options.config, method, path, body);
|
||
return sendJsonRequest({
|
||
config: options.config,
|
||
fetchImpl,
|
||
url: request.url,
|
||
method,
|
||
body,
|
||
errorPrefix: "Jira request failed",
|
||
});
|
||
}
|
||
|
||
return {
|
||
async searchIssues(input: SearchInput): Promise<CommandOutput<unknown>> {
|
||
const raw = (await send("POST", "/rest/api/3/search", {
|
||
jql: input.jql,
|
||
maxResults: input.maxResults,
|
||
startAt: input.startAt,
|
||
fields: [...ISSUE_FIELDS],
|
||
})) as Record<string, unknown>;
|
||
|
||
const issues = Array.isArray(raw.issues) ? raw.issues : [];
|
||
|
||
return {
|
||
ok: true,
|
||
data: {
|
||
issues: issues.map((issue) => normalizeIssue(options.config, issue as Record<string, unknown>)),
|
||
startAt: Number(raw.startAt ?? input.startAt),
|
||
maxResults: Number(raw.maxResults ?? input.maxResults),
|
||
total: Number(raw.total ?? issues.length),
|
||
},
|
||
};
|
||
},
|
||
|
||
async getIssue(issue: string): Promise<CommandOutput<unknown>> {
|
||
const url = new URL(`/rest/api/3/issue/${issue}`, `${options.config.jiraBaseUrl}/`);
|
||
url.searchParams.set("fields", ISSUE_FIELDS.join(","));
|
||
|
||
const raw = (await send("GET", `${url.pathname}${url.search}`)) as Record<string, unknown>;
|
||
|
||
return {
|
||
ok: true,
|
||
data: {
|
||
issue: normalizeIssue(options.config, raw),
|
||
},
|
||
raw,
|
||
};
|
||
},
|
||
|
||
async getTransitions(issue: string): Promise<CommandOutput<unknown>> {
|
||
const raw = (await send(
|
||
"GET",
|
||
`/rest/api/3/issue/${issue}/transitions`,
|
||
)) as { transitions?: Array<Record<string, unknown>> };
|
||
|
||
return {
|
||
ok: true,
|
||
data: {
|
||
transitions: (raw.transitions ?? []).map((transition) => ({
|
||
id: String(transition.id ?? ""),
|
||
name: String(transition.name ?? ""),
|
||
toStatus: String(((transition.to ?? {}) as Record<string, unknown>).name ?? ""),
|
||
hasScreen: Boolean(transition.hasScreen),
|
||
})),
|
||
},
|
||
};
|
||
},
|
||
|
||
async createIssue(input: CreateInput): Promise<CommandOutput<unknown>> {
|
||
const project = input.project || options.config.defaultProject;
|
||
|
||
if (!project) {
|
||
throw new Error("jira-create requires --project or ATLASSIAN_DEFAULT_PROJECT");
|
||
}
|
||
|
||
const request = createRequest(options.config, "POST", "/rest/api/3/issue", {
|
||
fields: {
|
||
project: { key: project },
|
||
issuetype: { name: input.type },
|
||
summary: input.summary,
|
||
...(input.description ? { description: markdownToAdf(input.description) } : {}),
|
||
},
|
||
});
|
||
|
||
if (input.dryRun) {
|
||
return {
|
||
ok: true,
|
||
dryRun: true,
|
||
data: request,
|
||
};
|
||
}
|
||
|
||
const raw = await send("POST", "/rest/api/3/issue", request.body);
|
||
return { ok: true, data: raw };
|
||
},
|
||
|
||
async updateIssue(input: UpdateInput): Promise<CommandOutput<unknown>> {
|
||
const fields: Record<string, unknown> = {};
|
||
|
||
if (input.summary) {
|
||
fields.summary = input.summary;
|
||
}
|
||
|
||
if (input.description) {
|
||
fields.description = markdownToAdf(input.description);
|
||
}
|
||
|
||
if (Object.keys(fields).length === 0) {
|
||
throw new Error("jira-update requires --summary and/or --description-file");
|
||
}
|
||
|
||
const request = createRequest(options.config, "PUT", `/rest/api/3/issue/${input.issue}`, {
|
||
fields,
|
||
});
|
||
|
||
if (input.dryRun) {
|
||
return {
|
||
ok: true,
|
||
dryRun: true,
|
||
data: request,
|
||
};
|
||
}
|
||
|
||
await send("PUT", `/rest/api/3/issue/${input.issue}`, request.body);
|
||
return {
|
||
ok: true,
|
||
data: {
|
||
issue: input.issue,
|
||
updated: true,
|
||
},
|
||
};
|
||
},
|
||
|
||
async commentIssue(input: CommentInput): Promise<CommandOutput<unknown>> {
|
||
const request = createRequest(options.config, "POST", `/rest/api/3/issue/${input.issue}/comment`, {
|
||
body: markdownToAdf(input.body),
|
||
});
|
||
|
||
if (input.dryRun) {
|
||
return {
|
||
ok: true,
|
||
dryRun: true,
|
||
data: request,
|
||
};
|
||
}
|
||
|
||
const raw = await send("POST", `/rest/api/3/issue/${input.issue}/comment`, request.body);
|
||
return {
|
||
ok: true,
|
||
data: raw,
|
||
};
|
||
},
|
||
|
||
async transitionIssue(input: TransitionInput): Promise<CommandOutput<unknown>> {
|
||
const request = createRequest(
|
||
options.config,
|
||
"POST",
|
||
`/rest/api/3/issue/${input.issue}/transitions`,
|
||
{
|
||
transition: {
|
||
id: input.transition,
|
||
},
|
||
},
|
||
);
|
||
|
||
if (input.dryRun) {
|
||
return {
|
||
ok: true,
|
||
dryRun: true,
|
||
data: request,
|
||
};
|
||
}
|
||
|
||
await send("POST", `/rest/api/3/issue/${input.issue}/transitions`, request.body);
|
||
return {
|
||
ok: true,
|
||
data: {
|
||
issue: input.issue,
|
||
transitioned: true,
|
||
transition: input.transition,
|
||
},
|
||
};
|
||
},
|
||
};
|
||
}
|