Files
Aleksey Shakhmatov 0c6e590273 refactor(extensions): simplify lib helpers, tool registration and kit-doctor
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.
2026-08-07 00:24:04 +03:00

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 ?? ""}`,
};
}