Files
ai-coding-skills/skills/atlassian/opencode/scripts/src/confluence.ts

293 lines
7.9 KiB
TypeScript

import { sendJsonRequest } from "./http.js";
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
type ConfluenceClientOptions = {
config: AtlassianConfig;
fetchImpl?: FetchLike;
};
type SearchInput = {
query: string;
maxResults: number;
startAt: number;
};
type CreateInput = {
space?: string;
title: string;
body: string;
dryRun?: boolean;
};
type UpdateInput = {
pageId: string;
title: string;
body: string;
dryRun?: boolean;
};
type CommentInput = {
pageId: string;
body: string;
dryRun?: boolean;
};
type PageSummary = {
id: string;
title: string;
type: string;
status?: string;
spaceId?: string;
url?: string;
};
function buildUrl(baseUrl: string, path: string) {
return new URL(path, `${baseUrl}/`).toString();
}
function normalizePage(baseUrl: string, page: Record<string, unknown>, excerpt?: string) {
const links = (page._links ?? {}) as Record<string, unknown>;
return {
id: String(page.id ?? ""),
title: String(page.title ?? ""),
type: String(page.type ?? "page"),
...(page.status ? { status: String(page.status) } : {}),
...(page.spaceId ? { spaceId: String(page.spaceId) } : {}),
...(excerpt ? { excerpt } : {}),
...(links.webui ? { url: `${baseUrl}${String(links.webui)}` } : {}),
};
}
export function createConfluenceClient(options: ConfluenceClientOptions) {
const config = options.config;
async function getPageForUpdate(pageId: string) {
return (await sendJsonRequest({
config,
fetchImpl: options.fetchImpl,
url: buildUrl(config.confluenceBaseUrl, `/wiki/api/v2/pages/${pageId}?body-format=storage`),
method: "GET",
errorPrefix: "Confluence request failed",
})) as Record<string, unknown>;
}
return {
async searchPages(input: SearchInput): Promise<CommandOutput<unknown>> {
const url = new URL("/wiki/rest/api/search", `${config.confluenceBaseUrl}/`);
url.searchParams.set("cql", input.query);
url.searchParams.set("limit", String(input.maxResults));
url.searchParams.set("start", String(input.startAt));
const raw = (await sendJsonRequest({
config,
fetchImpl: options.fetchImpl,
url: url.toString(),
method: "GET",
errorPrefix: "Confluence request failed",
})) as Record<string, unknown>;
const results = Array.isArray(raw.results) ? raw.results : [];
return {
ok: true,
data: {
pages: results.map((entry) => {
const result = entry as Record<string, unknown>;
return normalizePage(
config.baseUrl,
(result.content ?? {}) as Record<string, unknown>,
result.excerpt ? String(result.excerpt) : undefined,
);
}),
startAt: Number(raw.start ?? input.startAt),
maxResults: Number(raw.limit ?? input.maxResults),
total: Number(raw.totalSize ?? raw.size ?? results.length),
},
};
},
async getPage(pageId: string): Promise<CommandOutput<unknown>> {
const raw = (await sendJsonRequest({
config,
fetchImpl: options.fetchImpl,
url: buildUrl(config.confluenceBaseUrl, `/wiki/api/v2/pages/${pageId}?body-format=storage`),
method: "GET",
errorPrefix: "Confluence request failed",
})) as Record<string, unknown>;
const body = ((raw.body ?? {}) as Record<string, unknown>).storage as Record<string, unknown> | undefined;
return {
ok: true,
data: {
page: {
...normalizePage(config.baseUrl, raw),
version: Number((((raw.version ?? {}) as Record<string, unknown>).number ?? 0)),
body: body?.value ? String(body.value) : "",
},
},
raw,
};
},
async listChildren(pageId: string, maxResults: number, startAt: number): Promise<CommandOutput<unknown>> {
const url = new URL(`/wiki/api/v2/pages/${pageId}/direct-children`, `${config.confluenceBaseUrl}/`);
url.searchParams.set("limit", String(maxResults));
url.searchParams.set("cursor", String(startAt));
const raw = (await sendJsonRequest({
config,
fetchImpl: options.fetchImpl,
url: url.toString(),
method: "GET",
errorPrefix: "Confluence request failed",
})) as Record<string, unknown>;
const results = Array.isArray(raw.results) ? raw.results : [];
const links = (raw._links ?? {}) as Record<string, unknown>;
return {
ok: true,
data: {
pages: results.map((page) => normalizePage(config.baseUrl, page as Record<string, unknown>)),
nextCursor: links.next ? String(links.next) : null,
},
};
},
async createPage(input: CreateInput): Promise<CommandOutput<unknown>> {
const spaceId = input.space || config.defaultSpace;
if (!spaceId) {
throw new Error("conf-create requires --space or ATLASSIAN_DEFAULT_SPACE");
}
const request = {
method: "POST" as const,
url: buildUrl(config.confluenceBaseUrl, "/wiki/api/v2/pages"),
body: {
spaceId,
title: input.title,
status: "current",
body: {
representation: "storage",
value: input.body,
},
},
};
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
const raw = await sendJsonRequest({
config,
fetchImpl: options.fetchImpl,
url: request.url,
method: request.method,
body: request.body,
errorPrefix: "Confluence request failed",
});
return {
ok: true,
data: raw,
};
},
async updatePage(input: UpdateInput): Promise<CommandOutput<unknown>> {
const currentPage = await getPageForUpdate(input.pageId);
const version = (((currentPage.version ?? {}) as Record<string, unknown>).number ?? 0) as number;
const spaceId = String(currentPage.spaceId ?? "");
const request = {
method: "PUT" as const,
url: buildUrl(config.confluenceBaseUrl, `/wiki/api/v2/pages/${input.pageId}`),
body: {
id: input.pageId,
status: String(currentPage.status ?? "current"),
title: input.title,
spaceId,
version: {
number: Number(version) + 1,
},
body: {
representation: "storage",
value: input.body,
},
},
};
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
const raw = await sendJsonRequest({
config,
fetchImpl: options.fetchImpl,
url: request.url,
method: request.method,
body: request.body,
errorPrefix: "Confluence request failed",
handleResponseError(response) {
if (response.status === 409) {
return new Error(`Confluence update conflict: page ${input.pageId} was updated by someone else`);
}
return undefined;
},
});
return {
ok: true,
data: raw,
};
},
async commentPage(input: CommentInput): Promise<CommandOutput<unknown>> {
const request = {
method: "POST" as const,
url: buildUrl(config.confluenceBaseUrl, "/wiki/api/v2/footer-comments"),
body: {
pageId: input.pageId,
body: {
representation: "storage",
value: input.body,
},
},
};
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
const raw = await sendJsonRequest({
config,
fetchImpl: options.fetchImpl,
url: request.url,
method: request.method,
body: request.body,
errorPrefix: "Confluence request failed",
});
return {
ok: true,
data: raw,
};
},
};
}