Behavior-preserving cleanups: - extract shared resolveBaseUrl helper for jira/confluence base-URL logic - extract shared registerTool helper; port jira/gitlab/confluence tools to it - make scanSecrets declarative (filter/map) - collapse ok/warn pair into status() and drop double existsSync in kit-doctor Typecheck clean; npm test 71 PASS, 0 FAIL.
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
/**
|
|
* Pure REST helpers for the corporate wiki (Confluence Server/DC, rest/api).
|
|
* `docsUrl` comes from config/company.json; `CONFLUENCE_TOKEN` from the env.
|
|
* No pi API — unit-testable.
|
|
*/
|
|
import { apiRequest, requireToken, ApiError, resolveBaseUrl } from "./atlassian.ts";
|
|
import { readLocalConfig } from "./company-config.ts";
|
|
import * as path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
const CONFIG_PATH = path.join(HERE, "..", "..", "config", "company.json");
|
|
|
|
/** Resolve the Confluence base URL from company.json (env override for labs). */
|
|
export function confluenceBaseUrl(): string {
|
|
const cfg = readLocalConfig(CONFIG_PATH);
|
|
const url = resolveBaseUrl("CONFLUENCE_URL", cfg.docsUrl);
|
|
if (!url) throw new ApiError("Не задан docsUrl в config/company.json и нет CONFLUENCE_URL.");
|
|
return url;
|
|
}
|
|
|
|
function token(): string {
|
|
return requireToken(
|
|
"CONFLUENCE_TOKEN",
|
|
"personal access token Confluence. Где брать: Профиль → Security → API tokens / Personal Access Tokens → создать",
|
|
);
|
|
}
|
|
|
|
export interface Page {
|
|
id: string;
|
|
title: string;
|
|
url?: string;
|
|
body?: string;
|
|
spaceKey?: string;
|
|
}
|
|
|
|
export interface SearchResult {
|
|
pages: Page[];
|
|
total: number;
|
|
}
|
|
|
|
/** CQL search for Confluence pages. `cql` supports the standard text/title/space operators. */
|
|
export async function search(cql: string, opts: { limit?: number; expandBody?: boolean } = {}): Promise<SearchResult> {
|
|
const base = confluenceBaseUrl();
|
|
if (!cql) throw new ApiError("search требует CQL-запрос (например 'text=\"foo\"').");
|
|
const data: any = await apiRequest(base, token(), "/rest/api/content/search", {
|
|
query: { cql, limit: opts.limit ?? 25, expand: opts.expandBody ? "body.view" : undefined },
|
|
});
|
|
const pages: Page[] = (data?.results ?? []).map((p: any) => ({
|
|
id: p.id,
|
|
title: p.title ?? "",
|
|
spaceKey: p.space?.key,
|
|
body: opts.expandBody ? (p.body?.view?.value ?? "") : undefined,
|
|
url: `${base}/spaces/${p.space?.key ?? "~self"}/pages/${p.id}`,
|
|
}));
|
|
return { pages, total: data?.size ?? pages.length };
|
|
}
|
|
|
|
export async function pageGet(id: string, opts: { expandBody?: boolean } = {}): Promise<Page> {
|
|
const base = confluenceBaseUrl();
|
|
if (!id) throw new ApiError("pageGet требует id страницы.");
|
|
const data: any = await apiRequest(base, token(), `/rest/api/content/${encodeURIComponent(id)}`, {
|
|
query: opts.expandBody ? { expand: "body.view,version" } : undefined,
|
|
});
|
|
return {
|
|
id: data?.id ?? id,
|
|
title: data?.title ?? "",
|
|
spaceKey: data?.space?.key,
|
|
body: opts.expandBody ? (data?.body?.view?.value ?? "") : undefined,
|
|
url: `${base}/spaces/${data?.space?.key ?? "~self"}/pages/${data?.id ?? id}`,
|
|
};
|
|
}
|
|
|
|
export interface PageCreateInput {
|
|
spaceKey: string;
|
|
title: string;
|
|
body: string; // HTML (Storage format) or text
|
|
parentId?: string;
|
|
representation?: "storage" | "editor" | "wiki";
|
|
}
|
|
|
|
/** Create a new Confluence page. `body` is HTML in Storage format by default. */
|
|
export async function pageCreate(input: PageCreateInput): Promise<Page> {
|
|
const base = confluenceBaseUrl();
|
|
if (!input.spaceKey || !input.title || !input.body) {
|
|
throw new ApiError("pageCreate требует spaceKey, title и body.");
|
|
}
|
|
const rep = input.representation ?? "storage";
|
|
const body: Record<string, unknown> = {
|
|
type: "page",
|
|
title: input.title,
|
|
space: { key: input.spaceKey },
|
|
body: { [rep]: { value: input.body, representation: rep } },
|
|
};
|
|
if (input.parentId) (body as any).ancestors = [{ id: input.parentId }];
|
|
|
|
const data: any = await apiRequest(base, token(), "/rest/api/content", { method: "POST", body });
|
|
return {
|
|
id: data?.id ?? "",
|
|
title: data?.title ?? input.title,
|
|
spaceKey: data?.space?.key ?? input.spaceKey,
|
|
url: `${base}/spaces/${input.spaceKey}/pages/${data?.id ?? ""}`,
|
|
};
|
|
}
|