@cloudglab/confluence-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +34 -0
- package/CHANGELOG.md +26 -0
- package/README.md +147 -0
- package/assets/readme/confluence-cli-hero.png +0 -0
- package/assets/readme/confluence-cli-hero.svg +7 -0
- package/assets/readme/prompts/01-cover-confluence-cli.md +61 -0
- package/dist/api/endpoints.d.ts +404 -0
- package/dist/api/endpoints.js +85 -0
- package/dist/api/index.d.ts +148 -0
- package/dist/api/index.js +143 -0
- package/dist/bin/confluence-reader.d.ts +2 -0
- package/dist/bin/confluence-reader.js +8 -0
- package/dist/bin/confluence-writer.d.ts +2 -0
- package/dist/bin/confluence-writer.js +8 -0
- package/dist/bin/confluence.d.ts +2 -0
- package/dist/bin/confluence.js +11 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +154 -0
- package/dist/core/api-provider.d.ts +3 -0
- package/dist/core/api-provider.js +13 -0
- package/dist/core/changelog.d.ts +7 -0
- package/dist/core/changelog.js +42 -0
- package/dist/core/cli-output.d.ts +16 -0
- package/dist/core/cli-output.js +318 -0
- package/dist/core/cli-registry.d.ts +20 -0
- package/dist/core/cli-registry.js +148 -0
- package/dist/core/command-groups.generated.d.ts +2 -0
- package/dist/core/command-groups.generated.js +88 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +108 -0
- package/dist/core/http-error.d.ts +2 -0
- package/dist/core/http-error.js +4 -0
- package/dist/core/http.d.ts +28 -0
- package/dist/core/http.js +124 -0
- package/dist/core/inline-comment.d.ts +23 -0
- package/dist/core/inline-comment.js +27 -0
- package/dist/core/list-result.d.ts +14 -0
- package/dist/core/list-result.js +81 -0
- package/dist/core/manifest.d.ts +11 -0
- package/dist/core/manifest.js +42 -0
- package/dist/core/pagination.d.ts +26 -0
- package/dist/core/pagination.js +45 -0
- package/dist/core/roles.d.ts +4 -0
- package/dist/core/roles.js +12 -0
- package/dist/core/tool-registry.d.ts +9 -0
- package/dist/core/tool-registry.js +60 -0
- package/dist/core/validation.d.ts +2 -0
- package/dist/core/validation.js +10 -0
- package/dist/core/value.d.ts +2 -0
- package/dist/core/value.js +19 -0
- package/dist/core/write-guard.d.ts +25 -0
- package/dist/core/write-guard.js +49 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/install.d.ts +3 -0
- package/dist/install.js +407 -0
- package/dist/manifest.json +122 -0
- package/dist/tools/attachments.d.ts +2 -0
- package/dist/tools/attachments.js +46 -0
- package/dist/tools/content.d.ts +2 -0
- package/dist/tools/content.js +45 -0
- package/dist/tools/convert.d.ts +2 -0
- package/dist/tools/convert.js +63 -0
- package/dist/tools/init.d.ts +2 -0
- package/dist/tools/init.js +24 -0
- package/dist/tools/install.d.ts +2 -0
- package/dist/tools/install.js +52 -0
- package/dist/tools/labels.d.ts +2 -0
- package/dist/tools/labels.js +22 -0
- package/dist/tools/metadata.d.ts +2 -0
- package/dist/tools/metadata.js +26 -0
- package/dist/tools/rest.d.ts +2 -0
- package/dist/tools/rest.js +52 -0
- package/dist/tools/spaces.d.ts +2 -0
- package/dist/tools/spaces.js +18 -0
- package/dist/tools/transfer.d.ts +2 -0
- package/dist/tools/transfer.js +407 -0
- package/dist/types/common.d.ts +17 -0
- package/dist/types/common.js +2 -0
- package/dist/update-probe.d.ts +2 -0
- package/dist/update-probe.js +142 -0
- package/dist/utils/mark-metadata.d.ts +9 -0
- package/dist/utils/mark-metadata.js +16 -0
- package/dist/utils/markdown.d.ts +9 -0
- package/dist/utils/markdown.js +220 -0
- package/dist/utils/result.d.ts +3 -0
- package/dist/utils/result.js +7 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/docs/confluence-7.13.7-api.md +183 -0
- package/docs/index.html +608 -0
- package/docs/release.md +41 -0
- package/package.json +63 -0
- package/skills/confluence-cli/SKILL.md +63 -0
- package/skills/confluence-cli/reference/cli.md +36 -0
- package/skills/confluence-cli/reference/commands.md +41 -0
- package/skills/confluence-cli/reference/content.md +23 -0
- package/skills/confluence-cli/reference/overview.md +23 -0
- package/skills/confluence-cli/reference/rest.md +19 -0
- package/skills/confluence-cli/reference/transfer.md +27 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isRecord } from "./value.js";
|
|
5
|
+
const CONFIG_DIR = path.join(homedir(), ".confluence");
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
7
|
+
function normalizeRootUrl(url) {
|
|
8
|
+
const trimmed = url.trim();
|
|
9
|
+
const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
10
|
+
try {
|
|
11
|
+
const parsed = new URL(withProtocol);
|
|
12
|
+
return `${parsed.protocol}//${parsed.host}`.replace(/\/+$/, "");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return trimmed.replace(/\/+$/, "");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function normalizeApiBaseUrl(url) {
|
|
19
|
+
const trimmed = url.replace(/\/+$/, "");
|
|
20
|
+
return `${trimmed}/rest/api`;
|
|
21
|
+
}
|
|
22
|
+
export function normalizeConfig(raw) {
|
|
23
|
+
if (!raw.url?.trim())
|
|
24
|
+
throw new Error("缺少 Confluence 地址。");
|
|
25
|
+
const url = normalizeRootUrl(raw.url);
|
|
26
|
+
const apiBaseUrl = raw.apiBaseUrl ? raw.apiBaseUrl.replace(/\/+$/, "") : normalizeApiBaseUrl(url);
|
|
27
|
+
const personalToken = normalizeOptionalValue(raw.personalToken);
|
|
28
|
+
const username = normalizeOptionalValue(raw.username);
|
|
29
|
+
const password = normalizeOptionalValue(raw.password);
|
|
30
|
+
if (!personalToken && (!username || !password)) {
|
|
31
|
+
throw new Error("缺少凭证。需要 CONFLUENCE_PAT 或 CONFLUENCE_USERNAME + CONFLUENCE_PASSWORD。");
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
url,
|
|
35
|
+
apiBaseUrl,
|
|
36
|
+
authType: personalToken ? "pat" : "basic",
|
|
37
|
+
username,
|
|
38
|
+
password: personalToken ? undefined : password,
|
|
39
|
+
personalToken,
|
|
40
|
+
source: "~/.confluence/config.json",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function loadConfluenceConfig() {
|
|
44
|
+
const envConfig = {
|
|
45
|
+
url: normalizeOptionalValue(process.env.CONFLUENCE_URL),
|
|
46
|
+
apiBaseUrl: normalizeOptionalValue(process.env.CONFLUENCE_API_BASE_URL),
|
|
47
|
+
personalToken: normalizeOptionalValue(process.env.CONFLUENCE_PAT) ?? normalizeOptionalValue(process.env.CONFLUENCE_PERSONAL_TOKEN),
|
|
48
|
+
username: normalizeOptionalValue(process.env.CONFLUENCE_USERNAME),
|
|
49
|
+
password: normalizeOptionalValue(process.env.CONFLUENCE_PASSWORD) ?? normalizeOptionalValue(process.env.CONFLUENCE_API_TOKEN),
|
|
50
|
+
};
|
|
51
|
+
const envOverrides = removeEmptyValues(envConfig);
|
|
52
|
+
const hasAnyEnvOverride = Object.keys(envOverrides).length > 0;
|
|
53
|
+
if (envConfig.url && (envConfig.personalToken || (envConfig.username && envConfig.password))) {
|
|
54
|
+
return normalizeConfig(envOverrides);
|
|
55
|
+
}
|
|
56
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
57
|
+
throw new Error("未找到 Confluence 配置。\n" +
|
|
58
|
+
"用法: confluence initConfluence --url https://cf.cloudglab.cn --pat YOUR_TOKEN --save true\n" +
|
|
59
|
+
"或设置环境变量: CONFLUENCE_URL + CONFLUENCE_PAT");
|
|
60
|
+
}
|
|
61
|
+
const raw = readConfigFile();
|
|
62
|
+
if (!hasAnyEnvOverride)
|
|
63
|
+
return normalizeConfig(raw);
|
|
64
|
+
return normalizeConfig({
|
|
65
|
+
...raw,
|
|
66
|
+
...envOverrides,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function readConfigFile() {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
72
|
+
if (!isRecord(parsed))
|
|
73
|
+
throw new Error("配置内容必须是 JSON 对象");
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
+
throw new Error(`Confluence 配置文件损坏,请检查 ${CONFIG_FILE}:${message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function removeEmptyValues(config) {
|
|
82
|
+
return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined && value !== ""));
|
|
83
|
+
}
|
|
84
|
+
function normalizeOptionalValue(value) {
|
|
85
|
+
if (typeof value !== "string")
|
|
86
|
+
return undefined;
|
|
87
|
+
const trimmed = value.trim();
|
|
88
|
+
return trimmed === "" ? undefined : trimmed;
|
|
89
|
+
}
|
|
90
|
+
export function saveConfig(config) {
|
|
91
|
+
const normalized = normalizeConfig(config);
|
|
92
|
+
if (!existsSync(CONFIG_DIR))
|
|
93
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
94
|
+
writeFileSync(CONFIG_FILE, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
|
|
95
|
+
}
|
|
96
|
+
export function maskConfig(config) {
|
|
97
|
+
return {
|
|
98
|
+
...config,
|
|
99
|
+
password: config.password ? maskSecret(config.password) : undefined,
|
|
100
|
+
personalToken: config.personalToken ? maskSecret(config.personalToken) : undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function maskSecret(secret) {
|
|
104
|
+
if (secret.length <= 8)
|
|
105
|
+
return "********";
|
|
106
|
+
return `${secret.slice(0, 3)}***${secret.slice(-3)}`;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RestMethod } from "../api/endpoints.js";
|
|
2
|
+
import type { ConfluenceConfig } from "../types/common.js";
|
|
3
|
+
export interface MultipartFile {
|
|
4
|
+
fieldName: string;
|
|
5
|
+
filename: string;
|
|
6
|
+
contentType?: string;
|
|
7
|
+
data: Buffer;
|
|
8
|
+
}
|
|
9
|
+
export interface HttpError extends Error {
|
|
10
|
+
statusCode?: number;
|
|
11
|
+
responseBody?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare class ConfluenceHttpClient {
|
|
14
|
+
private readonly client;
|
|
15
|
+
constructor(config: ConfluenceConfig);
|
|
16
|
+
get<T>(path: string, params?: Record<string, unknown>): Promise<T>;
|
|
17
|
+
post<T>(path: string, data: unknown): Promise<T>;
|
|
18
|
+
put<T>(path: string, data: unknown): Promise<T>;
|
|
19
|
+
delete<T>(path: string, params?: Record<string, unknown>): Promise<T>;
|
|
20
|
+
request<T>(method: RestMethod, path: string, params?: Record<string, unknown>, data?: unknown): Promise<T>;
|
|
21
|
+
postMultipart<T>(path: string, fields: Record<string, string | boolean | number | undefined>, files: MultipartFile[]): Promise<T>;
|
|
22
|
+
putMultipart<T>(path: string, fields: Record<string, string | boolean | number | undefined>, files: MultipartFile[]): Promise<T>;
|
|
23
|
+
getBuffer(path: string, params?: Record<string, unknown>): Promise<{
|
|
24
|
+
data: Buffer;
|
|
25
|
+
headers: Record<string, unknown>;
|
|
26
|
+
}>;
|
|
27
|
+
private multipart;
|
|
28
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
export class ConfluenceHttpClient {
|
|
3
|
+
client;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.client = axios.create({
|
|
6
|
+
baseURL: config.apiBaseUrl,
|
|
7
|
+
timeout: 30_000,
|
|
8
|
+
auth: config.authType === "basic" ? { username: config.username, password: config.password } : undefined,
|
|
9
|
+
headers: {
|
|
10
|
+
Accept: "application/json",
|
|
11
|
+
...(config.authType === "pat" ? { Authorization: `Bearer ${config.personalToken}` } : {}),
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async get(path, params) {
|
|
16
|
+
try {
|
|
17
|
+
const response = await this.client.get(path, { params });
|
|
18
|
+
return response.data;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw normalizeHttpError(error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async post(path, data) {
|
|
25
|
+
try {
|
|
26
|
+
const response = await this.client.post(path, data, { headers: { "Content-Type": "application/json" } });
|
|
27
|
+
return response.data;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw normalizeHttpError(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async put(path, data) {
|
|
34
|
+
try {
|
|
35
|
+
const response = await this.client.put(path, data, { headers: { "Content-Type": "application/json" } });
|
|
36
|
+
return response.data;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw normalizeHttpError(error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async delete(path, params) {
|
|
43
|
+
try {
|
|
44
|
+
const response = await this.client.delete(path, { params });
|
|
45
|
+
return response.data;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw normalizeHttpError(error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async request(method, path, params, data) {
|
|
52
|
+
try {
|
|
53
|
+
const response = await this.client.request({ method, url: path, params, data, headers: data === undefined ? undefined : { "Content-Type": "application/json" } });
|
|
54
|
+
return response.data;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
throw normalizeHttpError(error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async postMultipart(path, fields, files) {
|
|
61
|
+
return this.multipart("POST", path, fields, files);
|
|
62
|
+
}
|
|
63
|
+
async putMultipart(path, fields, files) {
|
|
64
|
+
return this.multipart("PUT", path, fields, files);
|
|
65
|
+
}
|
|
66
|
+
async getBuffer(path, params) {
|
|
67
|
+
try {
|
|
68
|
+
const response = await this.client.get(path, { params, responseType: "arraybuffer" });
|
|
69
|
+
return { data: Buffer.from(response.data), headers: response.headers };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw normalizeHttpError(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async multipart(method, path, fields, files) {
|
|
76
|
+
const boundary = `----confluence-cli-${Date.now().toString(16)}`;
|
|
77
|
+
const body = buildMultipartBody(boundary, fields, files);
|
|
78
|
+
try {
|
|
79
|
+
const response = await this.client.request({
|
|
80
|
+
method,
|
|
81
|
+
url: path,
|
|
82
|
+
data: body,
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
85
|
+
"Content-Length": body.length,
|
|
86
|
+
"X-Atlassian-Token": "no-check",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
return response.data;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw normalizeHttpError(error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function buildMultipartBody(boundary, fields, files) {
|
|
97
|
+
const chunks = [];
|
|
98
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
99
|
+
if (value === undefined)
|
|
100
|
+
continue;
|
|
101
|
+
chunks.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${escapeMultipartName(name)}"\r\n\r\n${String(value)}\r\n`));
|
|
102
|
+
}
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
chunks.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${escapeMultipartName(file.fieldName)}"; filename="${escapeMultipartName(file.filename)}"\r\nContent-Type: ${file.contentType ?? "application/octet-stream"}\r\n\r\n`), file.data, Buffer.from("\r\n"));
|
|
105
|
+
}
|
|
106
|
+
chunks.push(Buffer.from(`--${boundary}--\r\n`));
|
|
107
|
+
return Buffer.concat(chunks);
|
|
108
|
+
}
|
|
109
|
+
function escapeMultipartName(value) {
|
|
110
|
+
return value.replace(/["\\\r\n]/g, "_");
|
|
111
|
+
}
|
|
112
|
+
function normalizeHttpError(error) {
|
|
113
|
+
if (axios.isAxiosError(error)) {
|
|
114
|
+
const status = error.response?.status;
|
|
115
|
+
const responseBody = error.response?.data;
|
|
116
|
+
const body = typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody ?? {});
|
|
117
|
+
const normalized = new Error(`Confluence request failed${status ? ` (${status})` : ""}: ${body || error.message}`);
|
|
118
|
+
normalized.statusCode = status;
|
|
119
|
+
normalized.responseBody = responseBody;
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline comment marker format for Confluence 7.13.7 (Server/Data Center):
|
|
3
|
+
*
|
|
4
|
+
* The page body storage format uses:
|
|
5
|
+
* <ac:inline-comment-marker ac:ref="ref-id">selected text</ac:inline-comment-marker>
|
|
6
|
+
*
|
|
7
|
+
* The comment is linked via `extensions.inlineProperties.inlineMarkerRef`.
|
|
8
|
+
*
|
|
9
|
+
* Known limitations:
|
|
10
|
+
* - Selection text must be contiguously present in the storage format body
|
|
11
|
+
* - Selection spanning XML element boundaries (e.g., across <strong>/</strong>) will fail
|
|
12
|
+
* - Selection within macros (ac:structured-macro) or links (ac:link) will fail
|
|
13
|
+
*/
|
|
14
|
+
export interface InlineAnnotationResult {
|
|
15
|
+
annotatedBody: string;
|
|
16
|
+
markerRef: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function generateMarkerRef(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Find `selection` in the storage format body and wrap it with an inline-comment-marker.
|
|
21
|
+
* Returns null if the selection cannot be safely annotated.
|
|
22
|
+
*/
|
|
23
|
+
export declare function findAndAnnotateSelection(storageBody: string, selection: string, markerRef?: string): InlineAnnotationResult | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export function generateMarkerRef() {
|
|
3
|
+
return `ic-${randomUUID().slice(0, 8)}`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Find `selection` in the storage format body and wrap it with an inline-comment-marker.
|
|
7
|
+
* Returns null if the selection cannot be safely annotated.
|
|
8
|
+
*/
|
|
9
|
+
export function findAndAnnotateSelection(storageBody, selection, markerRef) {
|
|
10
|
+
if (!selection || selection.trim().length === 0)
|
|
11
|
+
return null;
|
|
12
|
+
const index = storageBody.indexOf(selection);
|
|
13
|
+
if (index === -1)
|
|
14
|
+
return null;
|
|
15
|
+
// Safety: don't insert marker if it would split an XML tag
|
|
16
|
+
const charBefore = index > 0 ? storageBody[index - 1] : "";
|
|
17
|
+
const charAfter = index + selection.length < storageBody.length
|
|
18
|
+
? storageBody[index + selection.length]
|
|
19
|
+
: "";
|
|
20
|
+
if (charBefore === "<" || charAfter === ">")
|
|
21
|
+
return null;
|
|
22
|
+
const ref = markerRef ?? generateMarkerRef();
|
|
23
|
+
const marker = `<ac:inline-comment-marker ac:ref="${ref}">${selection}</ac:inline-comment-marker>`;
|
|
24
|
+
const annotatedBody = storageBody.slice(0, index) + marker + storageBody.slice(index + selection.length);
|
|
25
|
+
return { annotatedBody, markerRef: ref };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=inline-comment.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type PaginationInput } from "./pagination.js";
|
|
2
|
+
export interface ListResult<T = unknown> {
|
|
3
|
+
source: "server-paginated" | "server-full-list" | "client-paginated";
|
|
4
|
+
partial: boolean;
|
|
5
|
+
page: number;
|
|
6
|
+
limit: number;
|
|
7
|
+
total: number;
|
|
8
|
+
scanned?: number;
|
|
9
|
+
itemKey: string;
|
|
10
|
+
items: T[];
|
|
11
|
+
}
|
|
12
|
+
export declare function extractItems<T = unknown>(response: unknown, keys: string[]): T[];
|
|
13
|
+
export declare function toServerListResult<T = unknown>(response: unknown, keys: string[], pagination?: PaginationInput): ListResult<T>;
|
|
14
|
+
export declare function toClientPaginatedListResult<T = unknown>(response: unknown, keys: string[], pagination?: PaginationInput): ListResult<T>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { normalizePagination } from "./pagination.js";
|
|
2
|
+
export function extractItems(response, keys) {
|
|
3
|
+
if (Array.isArray(response))
|
|
4
|
+
return response;
|
|
5
|
+
if (typeof response !== "object" || response === null)
|
|
6
|
+
return [];
|
|
7
|
+
const record = response;
|
|
8
|
+
for (const key of keys) {
|
|
9
|
+
const value = record[key];
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
export function toServerListResult(response, keys, pagination = {}) {
|
|
16
|
+
const items = extractItems(response, keys);
|
|
17
|
+
const normalized = normalizePagination(pagination);
|
|
18
|
+
if (Array.isArray(response)) {
|
|
19
|
+
return {
|
|
20
|
+
source: "server-full-list",
|
|
21
|
+
partial: false,
|
|
22
|
+
page: 1,
|
|
23
|
+
limit: items.length,
|
|
24
|
+
total: items.length,
|
|
25
|
+
itemKey: keys[0] ?? "items",
|
|
26
|
+
items,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const record = typeof response === "object" && response !== null ? response : {};
|
|
30
|
+
const page = toPositiveInteger(record.page) ?? normalized.page;
|
|
31
|
+
const limit = toPositiveInteger(record.limit) ?? normalized.limit;
|
|
32
|
+
const total = toNonNegativeInteger(record.total) ?? items.length;
|
|
33
|
+
return {
|
|
34
|
+
source: "server-paginated",
|
|
35
|
+
partial: false,
|
|
36
|
+
page,
|
|
37
|
+
limit,
|
|
38
|
+
total,
|
|
39
|
+
itemKey: keys[0] ?? "items",
|
|
40
|
+
items,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function toClientPaginatedListResult(response, keys, pagination = {}) {
|
|
44
|
+
const items = extractItems(response, keys);
|
|
45
|
+
const normalized = normalizePagination(pagination);
|
|
46
|
+
const start = (normalized.page - 1) * normalized.limit;
|
|
47
|
+
const window = items.slice(start, start + normalized.limit);
|
|
48
|
+
return {
|
|
49
|
+
source: "client-paginated",
|
|
50
|
+
partial: false,
|
|
51
|
+
page: normalized.page,
|
|
52
|
+
limit: normalized.limit,
|
|
53
|
+
total: items.length,
|
|
54
|
+
scanned: items.length,
|
|
55
|
+
itemKey: keys[0] ?? "items",
|
|
56
|
+
items: window,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function toFiniteNumber(value) {
|
|
60
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
61
|
+
return value;
|
|
62
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
63
|
+
const parsed = Number(value);
|
|
64
|
+
if (Number.isFinite(parsed))
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
function toPositiveInteger(value) {
|
|
70
|
+
const parsed = toFiniteNumber(value);
|
|
71
|
+
if (parsed === undefined || parsed <= 0)
|
|
72
|
+
return undefined;
|
|
73
|
+
return Math.floor(parsed);
|
|
74
|
+
}
|
|
75
|
+
function toNonNegativeInteger(value) {
|
|
76
|
+
const parsed = toFiniteNumber(value);
|
|
77
|
+
if (parsed === undefined || parsed < 0)
|
|
78
|
+
return undefined;
|
|
79
|
+
return Math.floor(parsed);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=list-result.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { InMemoryCliRegistry } from "./cli-registry.js";
|
|
2
|
+
import type { Role } from "../types/common.js";
|
|
3
|
+
export interface Manifest {
|
|
4
|
+
version: string;
|
|
5
|
+
commands: string[];
|
|
6
|
+
groups: Record<string, string[]>;
|
|
7
|
+
commandToGroup: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadManifest(): Promise<Manifest | undefined>;
|
|
10
|
+
export declare function getAvailableCommandNames(role: Role): Promise<string[]>;
|
|
11
|
+
export declare function buildRegistryForCommand(role: Role, commandName: string): Promise<InMemoryCliRegistry>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { InMemoryCliRegistry } from "./cli-registry.js";
|
|
3
|
+
import { registerTools } from "./tool-registry.js";
|
|
4
|
+
import { hasToolGroup } from "./roles.js";
|
|
5
|
+
let manifestCache = null;
|
|
6
|
+
export async function loadManifest() {
|
|
7
|
+
if (manifestCache !== null)
|
|
8
|
+
return manifestCache ?? undefined;
|
|
9
|
+
try {
|
|
10
|
+
const manifestPath = new URL("../../dist/manifest.json", import.meta.url);
|
|
11
|
+
const content = await readFile(manifestPath, "utf8");
|
|
12
|
+
manifestCache = JSON.parse(content);
|
|
13
|
+
return manifestCache;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
manifestCache = undefined;
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function getAvailableCommandNames(role) {
|
|
21
|
+
const manifest = await loadManifest();
|
|
22
|
+
if (manifest) {
|
|
23
|
+
const names = new Set();
|
|
24
|
+
for (const [group, commands] of Object.entries(manifest.groups)) {
|
|
25
|
+
if (hasToolGroup(role, group)) {
|
|
26
|
+
for (const command of commands) {
|
|
27
|
+
names.add(command);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return Array.from(names).sort((left, right) => left.localeCompare(right));
|
|
32
|
+
}
|
|
33
|
+
const registry = new InMemoryCliRegistry();
|
|
34
|
+
await registerTools(registry, role);
|
|
35
|
+
return registry.list().map((command) => command.name);
|
|
36
|
+
}
|
|
37
|
+
export async function buildRegistryForCommand(role, commandName) {
|
|
38
|
+
const registry = new InMemoryCliRegistry();
|
|
39
|
+
await registerTools(registry, role, { commandName });
|
|
40
|
+
return registry;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface PaginationInput {
|
|
2
|
+
page?: number;
|
|
3
|
+
limit?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface PaginationParams {
|
|
6
|
+
page: number;
|
|
7
|
+
limit: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function normalizePagination(input?: PaginationInput): PaginationParams;
|
|
10
|
+
export declare function normalizeTotalPages(total: unknown, limit: number, fallbackItemCount?: number): number;
|
|
11
|
+
export declare function fetchRemainingPagesConcurrently<T>(firstPage: {
|
|
12
|
+
items: T[];
|
|
13
|
+
total?: unknown;
|
|
14
|
+
}, fetchPage: (page: number) => Promise<T[]>, options?: {
|
|
15
|
+
limit?: number;
|
|
16
|
+
concurrency?: number;
|
|
17
|
+
}): Promise<T[]>;
|
|
18
|
+
export interface FetchAllPagesOptions<T> {
|
|
19
|
+
fetchPage: (page: number) => Promise<{
|
|
20
|
+
items: T[];
|
|
21
|
+
total?: unknown;
|
|
22
|
+
}>;
|
|
23
|
+
pageSize?: number;
|
|
24
|
+
concurrency?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function fetchAllPages<T>(options: FetchAllPagesOptions<T>): Promise<T[]>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const DEFAULT_PAGE = 1;
|
|
2
|
+
const DEFAULT_LIMIT = 20;
|
|
3
|
+
const MAX_LIMIT = 100;
|
|
4
|
+
const MAX_TOTAL_PAGES = 1000;
|
|
5
|
+
export function normalizePagination(input = {}) {
|
|
6
|
+
const pageCandidate = Number.isFinite(input.page) && input.page && input.page > 0 ? Math.floor(input.page) : DEFAULT_PAGE;
|
|
7
|
+
const page = Math.max(pageCandidate, DEFAULT_PAGE);
|
|
8
|
+
const rawLimitCandidate = Number.isFinite(input.limit) && input.limit && input.limit > 0 ? Math.floor(input.limit) : DEFAULT_LIMIT;
|
|
9
|
+
const rawLimit = Math.max(rawLimitCandidate, 1);
|
|
10
|
+
const limit = Math.min(rawLimit, MAX_LIMIT);
|
|
11
|
+
return { page, limit };
|
|
12
|
+
}
|
|
13
|
+
export function normalizeTotalPages(total, limit, fallbackItemCount = 0) {
|
|
14
|
+
const parsedTotal = typeof total === "number" && Number.isFinite(total)
|
|
15
|
+
? total
|
|
16
|
+
: typeof total === "string" && total.trim() !== ""
|
|
17
|
+
? Number(total)
|
|
18
|
+
: fallbackItemCount;
|
|
19
|
+
const safeTotal = Number.isFinite(parsedTotal) && parsedTotal > 0 ? parsedTotal : fallbackItemCount;
|
|
20
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : DEFAULT_LIMIT;
|
|
21
|
+
return Math.min(Math.ceil(safeTotal / safeLimit), MAX_TOTAL_PAGES);
|
|
22
|
+
}
|
|
23
|
+
export async function fetchRemainingPagesConcurrently(firstPage, fetchPage, options = {}) {
|
|
24
|
+
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
25
|
+
const concurrency = options.concurrency ?? 3;
|
|
26
|
+
const totalPages = normalizeTotalPages(firstPage.total, limit, firstPage.items.length);
|
|
27
|
+
const allItems = [...firstPage.items];
|
|
28
|
+
if (totalPages <= 1)
|
|
29
|
+
return allItems;
|
|
30
|
+
for (let startPage = 2; startPage <= totalPages; startPage += concurrency) {
|
|
31
|
+
const endPage = Math.min(startPage + concurrency - 1, totalPages);
|
|
32
|
+
const pageIndexes = Array.from({ length: endPage - startPage + 1 }, (_, index) => startPage + index);
|
|
33
|
+
const pages = await Promise.all(pageIndexes.map((page) => fetchPage(page)));
|
|
34
|
+
for (const items of pages) {
|
|
35
|
+
allItems.push(...items);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return allItems;
|
|
39
|
+
}
|
|
40
|
+
export async function fetchAllPages(options) {
|
|
41
|
+
const pageSize = options.pageSize ?? 100;
|
|
42
|
+
const firstPage = await options.fetchPage(1);
|
|
43
|
+
return fetchRemainingPagesConcurrently({ items: firstPage.items, total: firstPage.total }, async (page) => (await options.fetchPage(page)).items, { limit: pageSize, concurrency: options.concurrency });
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=pagination.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Role } from "../types/common.js";
|
|
2
|
+
export type ToolGroup = "init" | "convert" | "metadata" | "rest" | "space" | "content" | "labels" | "attachments" | "transfer" | "install";
|
|
3
|
+
export declare function hasToolGroup(role: Role, group: ToolGroup): boolean;
|
|
4
|
+
export declare function getToolGroups(role: Role): ToolGroup[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const ROLE_TOOL_GROUPS = {
|
|
2
|
+
full: ["init", "install", "convert", "metadata", "rest", "space", "content", "labels", "attachments", "transfer"],
|
|
3
|
+
reader: ["init", "install", "convert", "metadata", "rest", "space", "content", "labels", "attachments"],
|
|
4
|
+
writer: ["init", "install", "convert", "metadata", "rest", "space", "content", "labels", "attachments", "transfer"],
|
|
5
|
+
};
|
|
6
|
+
export function hasToolGroup(role, group) {
|
|
7
|
+
return ROLE_TOOL_GROUPS[role].includes(group);
|
|
8
|
+
}
|
|
9
|
+
export function getToolGroups(role) {
|
|
10
|
+
return ROLE_TOOL_GROUPS[role];
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=roles.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CliRegistry } from "./cli-registry.js";
|
|
2
|
+
import type { Role } from "../types/common.js";
|
|
3
|
+
export interface RegisterToolsOptions {
|
|
4
|
+
commandName?: string;
|
|
5
|
+
onGroupRegister?: (group: ReturnType<typeof getToolGroupNames>[number], commands: string[]) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function registerTools(registry: CliRegistry, role: Role, options?: RegisterToolsOptions): Promise<void>;
|
|
8
|
+
declare function getToolGroupNames(): readonly ["init", "install", "convert", "metadata", "rest", "space", "content", "labels", "attachments", "transfer"];
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { hasToolGroup } from "./roles.js";
|
|
2
|
+
import { registerAttachmentTools } from "../tools/attachments.js";
|
|
3
|
+
import { registerConvertTools } from "../tools/convert.js";
|
|
4
|
+
import { registerContentTools } from "../tools/content.js";
|
|
5
|
+
import { registerInitTools } from "../tools/init.js";
|
|
6
|
+
import { registerInstallTools } from "../tools/install.js";
|
|
7
|
+
import { registerLabelTools } from "../tools/labels.js";
|
|
8
|
+
import { registerMetadataTools } from "../tools/metadata.js";
|
|
9
|
+
import { registerRestTools } from "../tools/rest.js";
|
|
10
|
+
import { registerSpaceTools } from "../tools/spaces.js";
|
|
11
|
+
import { registerTransferTools } from "../tools/transfer.js";
|
|
12
|
+
const groupLoaders = {
|
|
13
|
+
init: () => registerInitTools,
|
|
14
|
+
install: () => registerInstallTools,
|
|
15
|
+
convert: () => registerConvertTools,
|
|
16
|
+
metadata: () => registerMetadataTools,
|
|
17
|
+
rest: () => registerRestTools,
|
|
18
|
+
space: () => registerSpaceTools,
|
|
19
|
+
content: () => registerContentTools,
|
|
20
|
+
labels: () => registerLabelTools,
|
|
21
|
+
attachments: () => registerAttachmentTools,
|
|
22
|
+
transfer: () => registerTransferTools,
|
|
23
|
+
};
|
|
24
|
+
export async function registerTools(registry, role, options = {}) {
|
|
25
|
+
const { commandName, onGroupRegister } = options;
|
|
26
|
+
if (commandName) {
|
|
27
|
+
const group = await resolveCommandGroup(commandName);
|
|
28
|
+
if (group && hasToolGroup(role, group)) {
|
|
29
|
+
registerGroup(registry, group, onGroupRegister);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const group of getToolGroupNames()) {
|
|
34
|
+
if (!hasToolGroup(role, group))
|
|
35
|
+
continue;
|
|
36
|
+
registerGroup(registry, group, onGroupRegister);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function registerGroup(registry, group, onGroupRegister) {
|
|
40
|
+
const before = new Set(registry.list().map((command) => command.name));
|
|
41
|
+
const loader = groupLoaders[group];
|
|
42
|
+
const register = loader();
|
|
43
|
+
register(registry);
|
|
44
|
+
const added = registry.list().map((command) => command.name).filter((name) => !before.has(name));
|
|
45
|
+
onGroupRegister?.(group, added);
|
|
46
|
+
}
|
|
47
|
+
async function resolveCommandGroup(commandName) {
|
|
48
|
+
try {
|
|
49
|
+
const { commandToGroup } = await import("./command-groups.generated.js");
|
|
50
|
+
const group = commandToGroup[commandName];
|
|
51
|
+
return group;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function getToolGroupNames() {
|
|
58
|
+
return ["init", "install", "convert", "metadata", "rest", "space", "content", "labels", "attachments", "transfer"];
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=tool-registry.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 必填字符串:去空白后空串抛错,否则返回裁剪后的值。 */
|
|
2
|
+
export function requireNonBlank(value, message) {
|
|
3
|
+
if (typeof value !== "string")
|
|
4
|
+
throw new Error(message);
|
|
5
|
+
const text = value.trim();
|
|
6
|
+
if (text === "")
|
|
7
|
+
throw new Error(message);
|
|
8
|
+
return text;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=validation.js.map
|