251148c3ff
## Summary - add repository-wide quality tooling and verification scaffolding, including CI workflows, pnpm workspace setup, ESLint/Prettier/markdown checks, and generated-output verification helpers - reorganize skill sources and generation flow by introducing canonical `_source` variants, generator/manifests, reusable helper abstractions, and shared web-automation/browser utilities - clean up and expand documentation so the root README flows into docs and skill docs, with clearer development, reviewer, installer, and workflow guidance ## Notable changes - docs flow and consistency cleanup across `README.md`, `docs/README.md`, and related docs - new scripts for `check`, docs verification, generated-file verification, shell portability, and safe directory replacement - refactors in Atlassian and web-automation skill runtimes to reduce duplication and centralize reusable code - changelog, development documentation, and CI surface updates ## Test Plan - [ ] `pnpm run check` - [ ] review generated/manifests and skill sync outputs - [ ] smoke-check docs flow from `README.md` to `docs/README.md` to skill docs ## Notes - this branch currently includes tracked `skills/web-automation/shared/node_modules` content that should be reviewed carefully as potentially noisy/accidental committed artifacts Co-authored-by: Stefano Fiorini <stefano.fiorini@firsthorizon.com> Reviewed-on: #1
277 lines
7.9 KiB
TypeScript
277 lines
7.9 KiB
TypeScript
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||
import { dryRunResponse } from "./command-helpers.js";
|
||
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 dryRunResponse(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 dryRunResponse(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 dryRunResponse(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,
|
||
};
|
||
},
|
||
};
|
||
}
|