feat(atlassian): implement milestone M4 - packaging and doc sync
This commit is contained in:
292
skills/atlassian/opencode/scripts/src/confluence.ts
Normal file
292
skills/atlassian/opencode/scripts/src/confluence.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user