@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.
Files changed (100) hide show
  1. package/AGENTS.md +34 -0
  2. package/CHANGELOG.md +26 -0
  3. package/README.md +147 -0
  4. package/assets/readme/confluence-cli-hero.png +0 -0
  5. package/assets/readme/confluence-cli-hero.svg +7 -0
  6. package/assets/readme/prompts/01-cover-confluence-cli.md +61 -0
  7. package/dist/api/endpoints.d.ts +404 -0
  8. package/dist/api/endpoints.js +85 -0
  9. package/dist/api/index.d.ts +148 -0
  10. package/dist/api/index.js +143 -0
  11. package/dist/bin/confluence-reader.d.ts +2 -0
  12. package/dist/bin/confluence-reader.js +8 -0
  13. package/dist/bin/confluence-writer.d.ts +2 -0
  14. package/dist/bin/confluence-writer.js +8 -0
  15. package/dist/bin/confluence.d.ts +2 -0
  16. package/dist/bin/confluence.js +11 -0
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/cli.js +154 -0
  19. package/dist/core/api-provider.d.ts +3 -0
  20. package/dist/core/api-provider.js +13 -0
  21. package/dist/core/changelog.d.ts +7 -0
  22. package/dist/core/changelog.js +42 -0
  23. package/dist/core/cli-output.d.ts +16 -0
  24. package/dist/core/cli-output.js +318 -0
  25. package/dist/core/cli-registry.d.ts +20 -0
  26. package/dist/core/cli-registry.js +148 -0
  27. package/dist/core/command-groups.generated.d.ts +2 -0
  28. package/dist/core/command-groups.generated.js +88 -0
  29. package/dist/core/config.d.ts +5 -0
  30. package/dist/core/config.js +108 -0
  31. package/dist/core/http-error.d.ts +2 -0
  32. package/dist/core/http-error.js +4 -0
  33. package/dist/core/http.d.ts +28 -0
  34. package/dist/core/http.js +124 -0
  35. package/dist/core/inline-comment.d.ts +23 -0
  36. package/dist/core/inline-comment.js +27 -0
  37. package/dist/core/list-result.d.ts +14 -0
  38. package/dist/core/list-result.js +81 -0
  39. package/dist/core/manifest.d.ts +11 -0
  40. package/dist/core/manifest.js +42 -0
  41. package/dist/core/pagination.d.ts +26 -0
  42. package/dist/core/pagination.js +45 -0
  43. package/dist/core/roles.d.ts +4 -0
  44. package/dist/core/roles.js +12 -0
  45. package/dist/core/tool-registry.d.ts +9 -0
  46. package/dist/core/tool-registry.js +60 -0
  47. package/dist/core/validation.d.ts +2 -0
  48. package/dist/core/validation.js +10 -0
  49. package/dist/core/value.d.ts +2 -0
  50. package/dist/core/value.js +19 -0
  51. package/dist/core/write-guard.d.ts +25 -0
  52. package/dist/core/write-guard.js +49 -0
  53. package/dist/index.d.ts +3 -0
  54. package/dist/index.js +3 -0
  55. package/dist/install.d.ts +3 -0
  56. package/dist/install.js +407 -0
  57. package/dist/manifest.json +122 -0
  58. package/dist/tools/attachments.d.ts +2 -0
  59. package/dist/tools/attachments.js +46 -0
  60. package/dist/tools/content.d.ts +2 -0
  61. package/dist/tools/content.js +45 -0
  62. package/dist/tools/convert.d.ts +2 -0
  63. package/dist/tools/convert.js +63 -0
  64. package/dist/tools/init.d.ts +2 -0
  65. package/dist/tools/init.js +24 -0
  66. package/dist/tools/install.d.ts +2 -0
  67. package/dist/tools/install.js +52 -0
  68. package/dist/tools/labels.d.ts +2 -0
  69. package/dist/tools/labels.js +22 -0
  70. package/dist/tools/metadata.d.ts +2 -0
  71. package/dist/tools/metadata.js +26 -0
  72. package/dist/tools/rest.d.ts +2 -0
  73. package/dist/tools/rest.js +52 -0
  74. package/dist/tools/spaces.d.ts +2 -0
  75. package/dist/tools/spaces.js +18 -0
  76. package/dist/tools/transfer.d.ts +2 -0
  77. package/dist/tools/transfer.js +407 -0
  78. package/dist/types/common.d.ts +17 -0
  79. package/dist/types/common.js +2 -0
  80. package/dist/update-probe.d.ts +2 -0
  81. package/dist/update-probe.js +142 -0
  82. package/dist/utils/mark-metadata.d.ts +9 -0
  83. package/dist/utils/mark-metadata.js +16 -0
  84. package/dist/utils/markdown.d.ts +9 -0
  85. package/dist/utils/markdown.js +220 -0
  86. package/dist/utils/result.d.ts +3 -0
  87. package/dist/utils/result.js +7 -0
  88. package/dist/version.d.ts +1 -0
  89. package/dist/version.js +2 -0
  90. package/docs/confluence-7.13.7-api.md +183 -0
  91. package/docs/index.html +608 -0
  92. package/docs/release.md +41 -0
  93. package/package.json +63 -0
  94. package/skills/confluence-cli/SKILL.md +63 -0
  95. package/skills/confluence-cli/reference/cli.md +36 -0
  96. package/skills/confluence-cli/reference/commands.md +41 -0
  97. package/skills/confluence-cli/reference/content.md +23 -0
  98. package/skills/confluence-cli/reference/overview.md +23 -0
  99. package/skills/confluence-cli/reference/rest.md +19 -0
  100. package/skills/confluence-cli/reference/transfer.md +27 -0
@@ -0,0 +1,407 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, extname, join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { z } from "zod";
6
+ import { ConfluenceApi } from "../api/index.js";
7
+ import { loadConfluenceConfig } from "../core/config.js";
8
+ import { previewOrAssertWriteAllowed } from "../core/write-guard.js";
9
+ import { markdownToStorage, markdownToWiki, parseMarkdown, postProcessStorageHtml } from "../utils/markdown.js";
10
+ import { jsonResult } from "../utils/result.js";
11
+ export function registerTransferTools(registry) {
12
+ registry.tool("uploadMarkdown", z.object({
13
+ file: z.string(),
14
+ id: z.coerce.string().optional(),
15
+ space: z.string().optional(),
16
+ title: z.string().optional(),
17
+ parentId: z.coerce.string().optional(),
18
+ representation: z.enum(["wiki", "storage"]).default("wiki"),
19
+ attachments: z.array(z.string()).optional(),
20
+ mermaid: z.enum(["png", "svg", "none"]).default("png"),
21
+ forceReupload: z.boolean().default(false),
22
+ confirm: z.boolean().default(false),
23
+ toc: z.boolean().default(false),
24
+ tocMaxLevel: z.coerce.number().min(1).max(7).optional(),
25
+ }), async (input) => {
26
+ const representation = input.representation ?? "wiki";
27
+ const raw = readFileSync(input.file, "utf8");
28
+ const parsed = parseMarkdown(raw, basename(input.file, extname(input.file)));
29
+ const title = input.title ?? parsed.title;
30
+ if (!title)
31
+ throw new Error("Missing page title. Provide --title or an H1/frontmatter title.");
32
+ const prepared = prepareMarkdownForUpload(parsed.body, title, input.file, representation, input.mermaid ?? "png");
33
+ const effectiveRepresentation = prepared.generatedFiles.length > 0 ? "storage" : representation;
34
+ const attachments = [...prepared.generatedFiles.map((item) => item.filePath), ...(input.attachments ?? [])];
35
+ const body = input.toc ? buildTocMacro(input.tocMaxLevel) + "\n" + prepared.body : prepared.body;
36
+ const action = input.id ? "updateMarkdownPage" : "createMarkdownPage";
37
+ const preview = previewOrAssertWriteAllowed({ action, confirm: input.confirm, payload: { title, space: input.space, parentId: input.parentId, representation: effectiveRepresentation, attachments, forceReupload: input.forceReupload ?? false, bodyPreview: body.slice(0, 1000), generatedFiles: prepared.generatedFiles.map((item) => item.filePath) } });
38
+ if (preview)
39
+ return jsonResult(preview);
40
+ const config = loadConfluenceConfig();
41
+ const api = new ConfluenceApi(config);
42
+ let page;
43
+ if (input.id) {
44
+ const current = await api.getContent(input.id, "version,title");
45
+ page = await api.updateContent({ id: input.id, title, body, representation: effectiveRepresentation, version: (current.version?.number ?? 0) + 1, parentId: input.parentId });
46
+ }
47
+ else {
48
+ if (!input.space)
49
+ throw new Error("Creating content requires --space.");
50
+ page = await api.createContent({ space: input.space, title, body, representation: effectiveRepresentation, parentId: input.parentId });
51
+ }
52
+ const uploadedAttachments = await uploadAttachmentFiles(api, page.id, attachments, input.forceReupload ?? false);
53
+ const finalBody = applyMermaidImageMacros(body, prepared.generatedFiles, uploadedAttachments);
54
+ if (finalBody !== body) {
55
+ const current = await api.getContent(page.id, "version,title");
56
+ page = await api.updateContent({ id: page.id, title, body: finalBody, representation: "storage", version: (current.version?.number ?? 0) + 1, parentId: input.parentId });
57
+ }
58
+ return jsonResult({ page, attachments: uploadedAttachments, generatedFiles: prepared.generatedFiles.map((item) => item.filePath) });
59
+ }, "Upload Markdown as Confluence page; writes require confirm=true");
60
+ registry.tool("uploadHtml", z.object({
61
+ file: z.string(),
62
+ id: z.coerce.string().optional(),
63
+ space: z.string().optional(),
64
+ title: z.string().optional(),
65
+ parentId: z.coerce.string().optional(),
66
+ attachments: z.array(z.string()).optional(),
67
+ mermaid: z.enum(["png", "svg", "none"]).default("png"),
68
+ forceReupload: z.boolean().default(false),
69
+ confirm: z.boolean().default(false),
70
+ toc: z.boolean().default(false),
71
+ tocMaxLevel: z.coerce.number().min(1).max(7).optional(),
72
+ }), async (input) => {
73
+ const raw = readFileSync(input.file, "utf8");
74
+ const title = input.title ?? extractTitleFromHtml(raw) ?? basename(input.file, extname(input.file));
75
+ const prepared = replaceMermaidInHtml(raw, title, input.file, input.mermaid ?? "png");
76
+ const bodyContent = extractBodyContent(prepared.html);
77
+ let body = postProcessStorageHtml(bodyContent);
78
+ if (input.toc)
79
+ body = buildTocMacro(input.tocMaxLevel) + "\n" + body;
80
+ const attachments = [...prepared.generatedFiles.map((item) => item.filePath), ...(input.attachments ?? [])];
81
+ const action = input.id ? "updateHtmlPage" : "createHtmlPage";
82
+ const preview = previewOrAssertWriteAllowed({ action, confirm: input.confirm, payload: { title, space: input.space, parentId: input.parentId, representation: "storage", attachments, forceReupload: input.forceReupload ?? false, bodyPreview: body.slice(0, 1000), generatedFiles: prepared.generatedFiles.map((item) => item.filePath) } });
83
+ if (preview)
84
+ return jsonResult(preview);
85
+ const config = loadConfluenceConfig();
86
+ const api = new ConfluenceApi(config);
87
+ let page;
88
+ if (input.id) {
89
+ const current = await api.getContent(input.id, "version,title");
90
+ page = await api.updateContent({ id: input.id, title, body, representation: "storage", version: (current.version?.number ?? 0) + 1, parentId: input.parentId });
91
+ }
92
+ else {
93
+ if (!input.space)
94
+ throw new Error("Creating content requires --space.");
95
+ page = await api.createContent({ space: input.space, title, body, representation: "storage", parentId: input.parentId });
96
+ }
97
+ const uploadedAttachments = await uploadAttachmentFiles(api, page.id, attachments, input.forceReupload ?? false);
98
+ const finalBody = applyMermaidImageMacros(body, prepared.generatedFiles, uploadedAttachments);
99
+ if (finalBody !== body) {
100
+ const current = await api.getContent(page.id, "version,title");
101
+ page = await api.updateContent({ id: page.id, title, body: finalBody, representation: "storage", version: (current.version?.number ?? 0) + 1, parentId: input.parentId });
102
+ }
103
+ return jsonResult({ page, attachments: uploadedAttachments, generatedFiles: prepared.generatedFiles.map((item) => item.filePath) });
104
+ }, "Upload HTML as Confluence page; writes require confirm=true");
105
+ registry.tool("downloadPage", z.object({
106
+ id: z.coerce.string(),
107
+ outputDir: z.string().default("."),
108
+ saveHtml: z.boolean().default(false),
109
+ downloadAttachments: z.boolean().default(false),
110
+ downloadChildren: z.boolean().default(false),
111
+ }), async ({ id, outputDir, saveHtml, downloadAttachments, downloadChildren }) => {
112
+ const targetDir = outputDir ?? ".";
113
+ const config = loadConfluenceConfig();
114
+ const api = new ConfluenceApi(config);
115
+ mkdirSync(targetDir, { recursive: true });
116
+ return jsonResult(await downloadPageToDir(api, config, id, targetDir, { saveHtml: saveHtml ?? false, downloadAttachments: downloadAttachments ?? false, downloadChildren: downloadChildren ?? false }));
117
+ }, "Download one page to Markdown with optional attachments and children");
118
+ }
119
+ function prepareMarkdownForUpload(markdown, pageTitle, sourceFile, representation, mermaidMode) {
120
+ const prepared = replaceMermaidFences(markdown, pageTitle, sourceFile, mermaidMode);
121
+ const body = prepared.generatedFiles.length > 0 || representation === "storage" ? markdownToStorage(prepared.markdown) : markdownToWiki(prepared.markdown);
122
+ return { body, generatedFiles: prepared.generatedFiles };
123
+ }
124
+ function replaceMermaidFences(markdown, pageTitle, sourceFile, mermaidMode) {
125
+ if (mermaidMode === "none")
126
+ return { markdown, generatedFiles: [] };
127
+ const lines = markdown.split(/\r?\n/);
128
+ const output = [];
129
+ const generatedFiles = [];
130
+ let inMermaid = false;
131
+ let buffer = [];
132
+ let index = 0;
133
+ for (const line of lines) {
134
+ const fence = line.match(/^```\s*mermaid\s*$/i);
135
+ if (fence) {
136
+ if (!inMermaid) {
137
+ inMermaid = true;
138
+ buffer = [];
139
+ }
140
+ else {
141
+ const mermaidSource = buffer.join("\n").trim();
142
+ const generated = createMermaidFile(mermaidSource, pageTitle, sourceFile, index, mermaidMode);
143
+ generatedFiles.push(generated);
144
+ output.push(generated.marker);
145
+ index += 1;
146
+ inMermaid = false;
147
+ }
148
+ continue;
149
+ }
150
+ if (inMermaid && /^```\s*$/.test(line)) {
151
+ const mermaidSource = buffer.join("\n").trim();
152
+ const generated = createMermaidFile(mermaidSource, pageTitle, sourceFile, index, mermaidMode);
153
+ generatedFiles.push(generated);
154
+ output.push(generated.marker);
155
+ index += 1;
156
+ inMermaid = false;
157
+ continue;
158
+ }
159
+ if (inMermaid) {
160
+ buffer.push(line);
161
+ continue;
162
+ }
163
+ output.push(line);
164
+ }
165
+ if (inMermaid)
166
+ throw new Error("Mermaid code fence is not closed.");
167
+ return { markdown: output.join("\n"), generatedFiles };
168
+ }
169
+ function applyMermaidImageMacros(body, generatedFiles, uploadedAttachments) {
170
+ let output = body;
171
+ for (let index = 0; index < generatedFiles.length; index += 1) {
172
+ const generated = generatedFiles[index];
173
+ const uploaded = uploadedAttachments[index];
174
+ const attachment = readUploadedAttachment(uploaded?.result);
175
+ if (!attachment)
176
+ continue;
177
+ const macro = buildImageMacro(attachment.title ?? generated.attachmentName);
178
+ output = output.replaceAll(`<p>${generated.marker}</p>`, macro).replaceAll(generated.marker, macro);
179
+ }
180
+ return output;
181
+ }
182
+ function buildImageMacro(filename) {
183
+ return `<ac:image><ri:attachment ri:filename="${escapeXml(filename)}" /></ac:image>`;
184
+ }
185
+ function readUploadedAttachment(value) {
186
+ if (typeof value !== "object" || value === null)
187
+ return undefined;
188
+ if ("results" in value && Array.isArray(value.results)) {
189
+ return value.results?.[0];
190
+ }
191
+ if ("id" in value)
192
+ return value;
193
+ return undefined;
194
+ }
195
+ function buildTocMacro(maxLevel) {
196
+ const macros = ['<ac:structured-macro ac:name="toc" ac:schema-version="1">'];
197
+ if (maxLevel !== undefined && maxLevel >= 1 && maxLevel <= 7) {
198
+ macros.push(`<ac:parameter ac:name="maxLevel">${maxLevel}</ac:parameter>`);
199
+ }
200
+ macros.push("</ac:structured-macro>");
201
+ macros.push('<ac:structured-macro ac:name="easy-heading-free" ac:schema-version="1" />');
202
+ return macros.join("");
203
+ }
204
+ function escapeXml(value) {
205
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;").replace(/'/g, "&apos;");
206
+ }
207
+ async function uploadAttachmentFiles(api, pageId, files, forceReupload) {
208
+ if (files.length === 0)
209
+ return [];
210
+ const existing = forceReupload ? await api.listAttachments(pageId, 100) : undefined;
211
+ const results = [];
212
+ for (const file of files) {
213
+ const filename = basename(file);
214
+ const matched = existing?.results.find((attachment) => attachment.title === filename);
215
+ const contentType = attachmentContentType(filename);
216
+ if (matched) {
217
+ results.push({ file, action: "updateAttachment", result: await api.updateAttachmentData({ pageId, attachmentId: matched.id, filename, data: readFileSync(file), minorEdit: true, contentType }) });
218
+ }
219
+ else {
220
+ results.push({ file, action: "uploadAttachment", result: await api.uploadAttachment({ pageId, filename, data: readFileSync(file), minorEdit: true, contentType }) });
221
+ }
222
+ }
223
+ return results;
224
+ }
225
+ function attachmentContentType(filename) {
226
+ const extension = extname(filename).toLowerCase();
227
+ if (extension === ".png")
228
+ return "image/png";
229
+ if (extension === ".svg")
230
+ return "image/svg+xml";
231
+ return undefined;
232
+ }
233
+ async function downloadPageToDir(api, config, id, targetDir, options) {
234
+ const page = await api.getContent(id, "body.storage,version,space,ancestors,metadata.labels");
235
+ const safeTitle = safeFileName(page.title);
236
+ const body = page.body?.storage?.value ?? "";
237
+ const markdownPath = join(targetDir, `${safeTitle}.md`);
238
+ const htmlPath = options.saveHtml ? join(targetDir, `${safeTitle}.html`) : undefined;
239
+ writeFileSync(markdownPath, markdownForPage(config, page, body));
240
+ if (htmlPath)
241
+ writeFileSync(htmlPath, body);
242
+ const attachmentPaths = options.downloadAttachments ? await downloadPageAttachments(api, page.id, join(targetDir, `${safeTitle}_attachments`)) : [];
243
+ const children = options.downloadChildren ? await downloadChildren(api, config, page.id, join(targetDir, `${safeTitle}_Children`), options) : [];
244
+ return { page: { id: page.id, title: page.title }, markdownPath, htmlPath, attachmentPaths, children };
245
+ }
246
+ async function downloadPageAttachments(api, pageId, targetDir) {
247
+ const attachments = await api.listAttachments(pageId, 100);
248
+ if (attachments.results.length === 0)
249
+ return [];
250
+ mkdirSync(targetDir, { recursive: true });
251
+ const paths = [];
252
+ for (const attachment of attachments.results) {
253
+ const outputPath = await downloadAttachment(api, attachment, targetDir);
254
+ if (outputPath)
255
+ paths.push(outputPath);
256
+ }
257
+ return paths;
258
+ }
259
+ async function downloadAttachment(api, attachment, targetDir) {
260
+ if (!attachment._links?.download)
261
+ return undefined;
262
+ const outputPath = join(targetDir, safeFileName(attachment.title));
263
+ const downloaded = await api.downloadAttachment(attachment._links.download);
264
+ writeFileSync(outputPath, downloaded.data);
265
+ return outputPath;
266
+ }
267
+ async function downloadChildren(api, config, pageId, targetDir, options) {
268
+ const children = await api.getChildren(pageId, "page", "body.storage,version,space", 100);
269
+ if (children.results.length === 0)
270
+ return [];
271
+ mkdirSync(targetDir, { recursive: true });
272
+ const childOptions = { ...options, downloadChildren: false };
273
+ const results = [];
274
+ for (const child of children.results) {
275
+ results.push(await downloadPageToDir(api, config, child.id, targetDir, childOptions));
276
+ }
277
+ return results;
278
+ }
279
+ function markdownForPage(config, page, body) {
280
+ return [
281
+ "---",
282
+ `title: ${JSON.stringify(page.title)}`,
283
+ `confluence_url: ${JSON.stringify(`${config.url}/pages/viewpage.action?pageId=${page.id}`)}`,
284
+ `id: ${page.id}`,
285
+ `space: ${page.space?.key ?? ""}`,
286
+ `version: ${page.version?.number ?? ""}`,
287
+ "---",
288
+ "",
289
+ htmlToPlainMarkdown(body),
290
+ ].join("\n");
291
+ }
292
+ function htmlToPlainMarkdown(html) {
293
+ return html
294
+ .replace(/<h([1-6])[^>]*>(.*?)<\/h\1>/gis, (_match, level, text) => `${"#".repeat(Number(level))} ${stripTags(text)}\n`)
295
+ .replace(/<br\s*\/?>/gi, "\n")
296
+ .replace(/<\/p>/gi, "\n\n")
297
+ .replace(/<\/li>/gi, "\n")
298
+ .replace(/<li[^>]*>/gi, "- ")
299
+ .replace(/<[^>]+>/g, "")
300
+ .replace(/&lt;/g, "<")
301
+ .replace(/&gt;/g, ">")
302
+ .replace(/&amp;/g, "&")
303
+ .trim();
304
+ }
305
+ function stripTags(value) {
306
+ return value.replace(/<[^>]+>/g, "").trim();
307
+ }
308
+ function extractBodyContent(html) {
309
+ const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
310
+ return bodyMatch ? bodyMatch[1].trim() : html;
311
+ }
312
+ function extractTitleFromHtml(html) {
313
+ const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
314
+ if (titleMatch)
315
+ return titleMatch[1].trim();
316
+ const h1Match = html.match(/<h1[^>]*>([^<]*)<\/h1>/i);
317
+ if (h1Match)
318
+ return h1Match[1].trim();
319
+ return undefined;
320
+ }
321
+ function replaceMermaidInHtml(html, pageTitle, sourceFile, mermaidMode) {
322
+ if (mermaidMode === "none")
323
+ return { html, generatedFiles: [] };
324
+ const generatedFiles = [];
325
+ let index = 0;
326
+ let output = html;
327
+ // 1) Handle HTML-style mermaid blocks: <pre><code class="language-mermaid">...</code></pre>
328
+ output = output.replace(/<pre><code\s+class="language-mermaid"[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_match, mermaidSource) => {
329
+ const source = mermaidSource.trim();
330
+ if (!source)
331
+ return _match;
332
+ const gf = createMermaidFile(source, pageTitle, sourceFile, index, mermaidMode);
333
+ generatedFiles.push(gf);
334
+ index += 1;
335
+ return gf.marker;
336
+ });
337
+ // 2) Handle markdown-style fences in HTML source: ```mermaid ... ```
338
+ const lines = output.split(/\r?\n/);
339
+ const outLines = [];
340
+ let inMermaid = false;
341
+ let buffer = [];
342
+ for (const line of lines) {
343
+ const fence = line.match(/^```\s*mermaid\s*$/i);
344
+ if (fence && !inMermaid) {
345
+ inMermaid = true;
346
+ buffer = [];
347
+ continue;
348
+ }
349
+ if (inMermaid && /^```\s*$/.test(line)) {
350
+ const source = buffer.join("\n").trim();
351
+ if (source) {
352
+ const gf = createMermaidFile(source, pageTitle, sourceFile, index, mermaidMode);
353
+ generatedFiles.push(gf);
354
+ index += 1;
355
+ outLines.push(gf.marker);
356
+ }
357
+ inMermaid = false;
358
+ continue;
359
+ }
360
+ if (inMermaid) {
361
+ buffer.push(line);
362
+ continue;
363
+ }
364
+ outLines.push(line);
365
+ }
366
+ return { html: outLines.join("\n"), generatedFiles };
367
+ }
368
+ function createMermaidFile(mermaidSource, pageTitle, sourceFile, idx, renderKind) {
369
+ const attachmentName = `${safeFileName(pageTitle || basename(sourceFile, extname(sourceFile)))}-mermaid-${idx + 1}.${renderKind}`;
370
+ const marker = `MERMAID_IMAGE_PLACEHOLDER_${idx}`;
371
+ const outputDir = join(tmpdir(), "confluence-cli");
372
+ mkdirSync(outputDir, { recursive: true });
373
+ const filePath = join(outputDir, attachmentName);
374
+ renderMermaidFile(mermaidSource, filePath, renderKind);
375
+ return { marker, filePath, attachmentName, renderKind };
376
+ }
377
+ function renderMermaidFile(mermaidSource, outputFile, renderKind) {
378
+ const inputFile = join(dirname(outputFile), `${basename(outputFile, extname(outputFile))}.mmd`);
379
+ writeFileSync(inputFile, mermaidSource, "utf8");
380
+ const mmdc = resolveMmdcBin();
381
+ const args = ["-i", inputFile, "-o", outputFile, "-b", "transparent"];
382
+ if (renderKind === "png") {
383
+ args.push("--scale", "3");
384
+ }
385
+ try {
386
+ execFileSync(mmdc, args, { stdio: "pipe" });
387
+ }
388
+ catch (error) {
389
+ const message = error instanceof Error ? error.message : String(error);
390
+ throw new Error(`Failed to render Mermaid as ${renderKind} with mermaid-cli: ${message}`);
391
+ }
392
+ }
393
+ function resolveMmdcBin() {
394
+ const extension = process.platform === "win32" ? ".cmd" : "";
395
+ const candidates = [
396
+ join(process.cwd(), "node_modules", ".bin", `mmdc${extension}`),
397
+ join(dirname(process.argv[1] ?? process.cwd()), "..", "node_modules", ".bin", `mmdc${extension}`),
398
+ ];
399
+ const found = candidates.find((candidate) => existsSync(candidate));
400
+ if (found)
401
+ return found;
402
+ return `mmdc${extension}`;
403
+ }
404
+ function safeFileName(value) {
405
+ return value.replace(/[\\/:*?"<>|]/g, "_");
406
+ }
407
+ //# sourceMappingURL=transfer.js.map
@@ -0,0 +1,17 @@
1
+ export type Role = "full" | "reader" | "writer";
2
+ export interface JsonContentResult {
3
+ content: Array<{
4
+ type: "text";
5
+ text: string;
6
+ }>;
7
+ }
8
+ export interface ConfluenceConfig {
9
+ url: string;
10
+ apiBaseUrl: string;
11
+ authType: "pat" | "basic";
12
+ username?: string;
13
+ password?: string;
14
+ personalToken?: string;
15
+ source: string;
16
+ }
17
+ export type ToolHandler<TInput> = (input: TInput) => Promise<JsonContentResult> | JsonContentResult;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1,2 @@
1
+ export declare function runDailyUpdateProbe(commandName?: string): Promise<void>;
2
+ export declare function writeUpdateCacheAfterInstall(version?: string): Promise<void>;
@@ -0,0 +1,142 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ import { isRecord } from "./core/value.js";
6
+ import { VERSION } from "./version.js";
7
+ const PACKAGE_NAME = "@cloudglab/confluence-cli";
8
+ const CHECK_FILE = path.join(homedir(), ".confluence", "update-check.json");
9
+ const SKIP_COMMANDS = new Set(["help", "list", "version", "install", "update", "upgrade", "uninstall", "remove", "--help", "-h", "--version", "-v"]);
10
+ export async function runDailyUpdateProbe(commandName) {
11
+ if (!commandName || SKIP_COMMANDS.has(commandName))
12
+ return;
13
+ if (process.env.NODE_ENV === "test")
14
+ return;
15
+ if (process.env.CONFLUENCE_SKIP_UPDATE_CHECK === "true")
16
+ return;
17
+ try {
18
+ const today = new Date().toISOString().slice(0, 10);
19
+ const state = await readUpdateCheckState();
20
+ notifyIfUpdateAvailable(state.latestVersion);
21
+ if (state.lastCheckedDate === today)
22
+ return;
23
+ await writeUpdateCheckState({ ...state, lastCheckedDate: today, currentVersion: VERSION });
24
+ triggerBackgroundVersionCheck();
25
+ }
26
+ catch (error) {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ process.stderr.write(`Confluence CLI 自动更新检查失败,已继续执行当前命令:${message}\n`);
29
+ }
30
+ }
31
+ export async function writeUpdateCacheAfterInstall(version) {
32
+ const today = new Date().toISOString().slice(0, 10);
33
+ await writeUpdateCheckState({
34
+ lastCheckedDate: today,
35
+ latestVersion: version ?? VERSION,
36
+ currentVersion: VERSION,
37
+ });
38
+ }
39
+ function notifyIfUpdateAvailable(latestVersion) {
40
+ if (!latestVersion || !isNewerVersion(latestVersion, VERSION))
41
+ return;
42
+ process.stderr.write([
43
+ `检测到 Confluence CLI 新版本 ${latestVersion}(当前 ${VERSION})。`,
44
+ "建议执行以下命令完成更新:",
45
+ " confluence update",
46
+ "如只更新工具且跳过配置校验,可执行:",
47
+ " confluence update --skip-config-check",
48
+ "",
49
+ ].join("\n"));
50
+ }
51
+ async function readUpdateCheckState() {
52
+ try {
53
+ const parsed = JSON.parse(await readFile(CHECK_FILE, "utf8"));
54
+ if (!isRecord(parsed))
55
+ return {};
56
+ return parsed;
57
+ }
58
+ catch {
59
+ return {};
60
+ }
61
+ }
62
+ async function writeUpdateCheckState(state) {
63
+ await mkdir(path.dirname(CHECK_FILE), { recursive: true, mode: 0o700 });
64
+ await writeFile(CHECK_FILE, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
65
+ }
66
+ function triggerBackgroundVersionCheck() {
67
+ const script = `
68
+ const { spawn } = require('child_process');
69
+ const { mkdirSync, writeFileSync } = require('fs');
70
+ const { homedir } = require('os');
71
+ const path = require('path');
72
+
73
+ const packageName = ${JSON.stringify(PACKAGE_NAME)};
74
+ const cliVersion = ${JSON.stringify(VERSION)};
75
+ const shell = ${process.platform === "win32"};
76
+
77
+ const npm = spawn('npm', ['view', packageName, 'version', '--silent'], {
78
+ shell,
79
+ detached: true,
80
+ stdio: ['ignore', 'pipe', 'ignore'],
81
+ });
82
+
83
+ let stdout = '';
84
+ npm.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
85
+
86
+ npm.on('close', (code) => {
87
+ if (code !== 0) return;
88
+ const latestVersion = stdout.trim();
89
+ if (!latestVersion) return;
90
+ const today = new Date().toISOString().slice(0, 10);
91
+ const checkFile = path.join(homedir(), '.confluence', 'update-check.json');
92
+ mkdirSync(path.dirname(checkFile), { recursive: true, mode: 0o700 });
93
+ writeFileSync(checkFile, JSON.stringify({ lastCheckedDate: today, latestVersion, currentVersion: cliVersion }, null, 2) + '\n', { mode: 0o600 });
94
+ });
95
+ `;
96
+ const child = spawn(process.execPath, ["-e", script], {
97
+ detached: true,
98
+ stdio: "ignore",
99
+ });
100
+ child.unref();
101
+ }
102
+ async function getLatestPackageVersion() {
103
+ const stdout = await runCommandOutput("npm", ["view", PACKAGE_NAME, "version", "--silent"]);
104
+ const version = stdout.trim();
105
+ if (!version)
106
+ throw new Error("npm view 没有返回最新版本号");
107
+ return version;
108
+ }
109
+ function runCommandOutput(command, args) {
110
+ return new Promise((resolve, reject) => {
111
+ const child = spawn(command, args, { shell: process.platform === "win32" });
112
+ let stdout = "";
113
+ let stderr = "";
114
+ child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
115
+ child.stderr?.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
116
+ child.on("error", reject);
117
+ child.on("close", (code) => {
118
+ if (code === 0) {
119
+ resolve(stdout);
120
+ return;
121
+ }
122
+ reject(new Error(`${command} ${args.join(" ")} 执行失败,退出码 ${String(code)}${stderr ? `:${stderr.trim()}` : ""}`));
123
+ });
124
+ });
125
+ }
126
+ function isNewerVersion(latestVersion, currentVersion) {
127
+ const latest = parseVersion(latestVersion);
128
+ const current = parseVersion(currentVersion);
129
+ for (let index = 0; index < Math.max(latest.length, current.length); index += 1) {
130
+ const latestPart = latest[index] ?? 0;
131
+ const currentPart = current[index] ?? 0;
132
+ if (latestPart > currentPart)
133
+ return true;
134
+ if (latestPart < currentPart)
135
+ return false;
136
+ }
137
+ return false;
138
+ }
139
+ function parseVersion(version) {
140
+ return version.replace(/^v/, "").split(/[.-]/).map((part) => Number.parseInt(part, 10)).map((part) => (Number.isFinite(part) ? part : 0));
141
+ }
142
+ //# sourceMappingURL=update-probe.js.map
@@ -0,0 +1,9 @@
1
+ export interface MarkMetadataInput {
2
+ space: string;
3
+ title?: string;
4
+ parents?: string[];
5
+ labels?: string[];
6
+ attachments?: string[];
7
+ }
8
+ export declare function createMarkMetadata(input: MarkMetadataInput): string;
9
+ export declare function removeMarkMetadata(content: string): string;
@@ -0,0 +1,16 @@
1
+ export function createMarkMetadata(input) {
2
+ const lines = [`<!-- Space: ${input.space} -->`];
3
+ if (input.title)
4
+ lines.push(`<!-- Title: ${input.title} -->`);
5
+ for (const parent of input.parents ?? [])
6
+ lines.push(`<!-- Parent: ${parent} -->`);
7
+ for (const label of input.labels ?? [])
8
+ lines.push(`<!-- Label: ${label} -->`);
9
+ for (const attachment of input.attachments ?? [])
10
+ lines.push(`<!-- Attachment: ${attachment} -->`);
11
+ return lines.join("\n");
12
+ }
13
+ export function removeMarkMetadata(content) {
14
+ return content.replace(/^<!--\s*(Space|Title|Parent|Label|Attachment):.*?-->\s*\r?\n/gim, "");
15
+ }
16
+ //# sourceMappingURL=mark-metadata.js.map
@@ -0,0 +1,9 @@
1
+ export interface ParsedMarkdown {
2
+ frontmatter: Record<string, unknown>;
3
+ body: string;
4
+ title?: string;
5
+ }
6
+ export declare function parseMarkdown(content: string, fallbackTitle?: string): ParsedMarkdown;
7
+ export declare function markdownToWiki(markdown: string): string;
8
+ export declare function markdownToStorage(markdown: string): string;
9
+ export declare function postProcessStorageHtml(html: string): string;