@repo-toolkit/confluence 0.8.0
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/LICENSE +201 -0
- package/README.md +95 -0
- package/cli.js +870 -0
- package/index.d.ts +179 -0
- package/index.js +781 -0
- package/package.json +46 -0
package/cli.js
ADDED
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseFlags as parseFlags2, resolveCliOptions as resolveCliOptions2, INTERACTIVE_FLAG } from "@repo-toolkit/publish-package";
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
8
|
+
import { dirname, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
9
|
+
import { parseFlags, resolveCliOptions, isPlainObject } from "@repo-toolkit/publish-package";
|
|
10
|
+
|
|
11
|
+
// src/confluence-client.ts
|
|
12
|
+
import { Buffer } from "buffer";
|
|
13
|
+
import { readFileSync } from "fs";
|
|
14
|
+
import { basename } from "path";
|
|
15
|
+
var V2_PATH = "/api/v2";
|
|
16
|
+
var V1_PATH = "/rest/api";
|
|
17
|
+
var DEFAULT_USER_AGENT = "repo-toolkit-confluence/1.0 (+node)";
|
|
18
|
+
var MAX_LIMIT = 250;
|
|
19
|
+
var STATUS_CODES = {
|
|
20
|
+
BAD_REQUEST: 400,
|
|
21
|
+
UNAUTHORIZED: 401,
|
|
22
|
+
FORBIDDEN: 403,
|
|
23
|
+
NOT_FOUND: 404,
|
|
24
|
+
CONFLICT: 409
|
|
25
|
+
};
|
|
26
|
+
var ConfluenceApiError = class extends Error {
|
|
27
|
+
constructor(message, status, endpoint, responseBody) {
|
|
28
|
+
super(`${message} (status=${status}, endpoint=${endpoint})`);
|
|
29
|
+
this.name = "ConfluenceApiError";
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.endpoint = endpoint;
|
|
32
|
+
this.responseBody = responseBody;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var ConfluenceClient = class {
|
|
36
|
+
constructor(options) {
|
|
37
|
+
if (!options.baseUrl) {
|
|
38
|
+
throw new Error("ConfluenceClient: baseUrl is required");
|
|
39
|
+
}
|
|
40
|
+
if (!options.username || !options.apiToken) {
|
|
41
|
+
throw new Error("ConfluenceClient: username and apiToken are required");
|
|
42
|
+
}
|
|
43
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
44
|
+
this.authHeader = "Basic " + Buffer.from(`${options.username}:${options.apiToken}`, "utf8").toString("base64");
|
|
45
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
46
|
+
this.userAgent = options.userAgent ?? DEFAULT_USER_AGENT;
|
|
47
|
+
}
|
|
48
|
+
async getSpaceIdByKey(spaceKey) {
|
|
49
|
+
const query = new URLSearchParams({ keys: spaceKey, limit: "1" });
|
|
50
|
+
const data = await this.requestJson(
|
|
51
|
+
this.v2Url(`/spaces?${query.toString()}`),
|
|
52
|
+
{ method: "GET" }
|
|
53
|
+
);
|
|
54
|
+
const result = data.results[0];
|
|
55
|
+
if (!result) {
|
|
56
|
+
throw new Error(`Confluence space not found for key: ${spaceKey}`);
|
|
57
|
+
}
|
|
58
|
+
return result.id;
|
|
59
|
+
}
|
|
60
|
+
async getPageByTitle(spaceId, title) {
|
|
61
|
+
const query = new URLSearchParams({
|
|
62
|
+
"space-id": spaceId,
|
|
63
|
+
title,
|
|
64
|
+
limit: "1",
|
|
65
|
+
"body-format": "storage"
|
|
66
|
+
});
|
|
67
|
+
const data = await this.requestJson(this.v2Url(`/pages?${query.toString()}`), {
|
|
68
|
+
method: "GET"
|
|
69
|
+
});
|
|
70
|
+
return data.results[0];
|
|
71
|
+
}
|
|
72
|
+
async getPage(pageId) {
|
|
73
|
+
return this.requestJson(this.v2Url(`/pages/${encodeURIComponent(pageId)}?body-format=storage`), {
|
|
74
|
+
method: "GET"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async createPage(input) {
|
|
78
|
+
const body = {
|
|
79
|
+
spaceId: input.spaceId,
|
|
80
|
+
status: input.status ?? "current",
|
|
81
|
+
title: input.title,
|
|
82
|
+
parentId: input.parentId,
|
|
83
|
+
body: {
|
|
84
|
+
representation: input.body.representation,
|
|
85
|
+
value: input.body.value
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return this.requestJson(this.v2Url("/pages"), {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "Content-Type": "application/json" },
|
|
91
|
+
body: JSON.stringify(body)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async updatePage(input) {
|
|
95
|
+
const body = {
|
|
96
|
+
id: input.id,
|
|
97
|
+
status: input.status ?? "current",
|
|
98
|
+
title: input.title,
|
|
99
|
+
body: {
|
|
100
|
+
representation: input.body.representation,
|
|
101
|
+
value: input.body.value
|
|
102
|
+
},
|
|
103
|
+
version: { number: input.version.number, message: input.version.message }
|
|
104
|
+
};
|
|
105
|
+
return this.requestJson(this.v2Url(`/pages/${encodeURIComponent(input.id)}`), {
|
|
106
|
+
method: "PUT",
|
|
107
|
+
headers: { "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify(body)
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async getAttachments(pageId) {
|
|
112
|
+
const results = [];
|
|
113
|
+
let cursor;
|
|
114
|
+
do {
|
|
115
|
+
const query = new URLSearchParams({ limit: String(MAX_LIMIT) });
|
|
116
|
+
if (cursor) {
|
|
117
|
+
query.set("cursor", cursor);
|
|
118
|
+
}
|
|
119
|
+
const data = await this.requestJson(
|
|
120
|
+
this.v2Url(`/pages/${encodeURIComponent(pageId)}/attachments?${query.toString()}`),
|
|
121
|
+
{ method: "GET" }
|
|
122
|
+
);
|
|
123
|
+
for (const item of data.results) {
|
|
124
|
+
results.push(item);
|
|
125
|
+
}
|
|
126
|
+
cursor = data._links?.next;
|
|
127
|
+
} while (cursor);
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
async uploadAttachment(pageId, filePath, comment) {
|
|
131
|
+
return this.sendAttachmentMultipart(pageId, void 0, filePath, comment);
|
|
132
|
+
}
|
|
133
|
+
async updateAttachmentData(pageId, attachmentId, filePath, comment) {
|
|
134
|
+
return this.sendAttachmentMultipart(pageId, attachmentId, filePath, comment);
|
|
135
|
+
}
|
|
136
|
+
async sendAttachmentMultipart(pageId, attachmentId, filePath, comment) {
|
|
137
|
+
const fileBuffer = readFileSync(filePath);
|
|
138
|
+
const filename = basename(filePath);
|
|
139
|
+
const boundary = "----repo-toolkit-confluence-" + Math.random().toString(16).slice(2);
|
|
140
|
+
const parts = [];
|
|
141
|
+
parts.push(multipartField(boundary, "file", filename, fileBuffer));
|
|
142
|
+
if (comment) {
|
|
143
|
+
parts.push(multipartField(boundary, "comment", void 0, Buffer.from(comment, "utf8")));
|
|
144
|
+
}
|
|
145
|
+
parts.push(Buffer.from(`--${boundary}--\r
|
|
146
|
+
`));
|
|
147
|
+
const endpoint = attachmentId ? this.v1Url(`/content/${encodeURIComponent(pageId)}/child/attachment/${encodeURIComponent(attachmentId)}/data`) : this.v1Url(`/content/${encodeURIComponent(pageId)}/child/attachment`);
|
|
148
|
+
const data = await this.requestJson(endpoint, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
152
|
+
"X-Atlassian-Token": "no-check"
|
|
153
|
+
},
|
|
154
|
+
body: Buffer.concat(parts)
|
|
155
|
+
});
|
|
156
|
+
return normalizeAttachmentResult(data);
|
|
157
|
+
}
|
|
158
|
+
async requestJson(endpoint, init) {
|
|
159
|
+
const headers = {
|
|
160
|
+
Authorization: this.authHeader,
|
|
161
|
+
Accept: "application/json",
|
|
162
|
+
"User-Agent": this.userAgent
|
|
163
|
+
};
|
|
164
|
+
if (init.headers) {
|
|
165
|
+
for (const [k, v] of Object.entries(init.headers)) {
|
|
166
|
+
headers[k] = v;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let response;
|
|
170
|
+
try {
|
|
171
|
+
response = await this.fetchFn(endpoint, {
|
|
172
|
+
method: init.method,
|
|
173
|
+
headers,
|
|
174
|
+
body: init.body
|
|
175
|
+
});
|
|
176
|
+
} catch (cause) {
|
|
177
|
+
throw new ConfluenceApiError(
|
|
178
|
+
cause instanceof Error ? `Network error: ${cause.message}` : "Network error",
|
|
179
|
+
0,
|
|
180
|
+
endpoint,
|
|
181
|
+
""
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const text = await response.text();
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
throw new ConfluenceApiError(describeStatus(response.status), response.status, endpoint, text);
|
|
187
|
+
}
|
|
188
|
+
if (text.length === 0) {
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(text);
|
|
193
|
+
} catch {
|
|
194
|
+
throw new ConfluenceApiError("Response was not valid JSON", response.status, endpoint, text);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
v2Url(path) {
|
|
198
|
+
return this.baseUrl + V2_PATH + path;
|
|
199
|
+
}
|
|
200
|
+
v1Url(path) {
|
|
201
|
+
return this.baseUrl + V1_PATH + path;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
function multipartField(boundary, name, filename, value) {
|
|
205
|
+
const headerLines = [`--${boundary}\r
|
|
206
|
+
`];
|
|
207
|
+
if (filename) {
|
|
208
|
+
headerLines.push(
|
|
209
|
+
`Content-Disposition: form-data; name="${name}"; filename="${filename}"\r
|
|
210
|
+
`,
|
|
211
|
+
"Content-Type: application/octet-stream\r\n"
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
headerLines.push(`Content-Disposition: form-data; name="${name}"\r
|
|
215
|
+
`);
|
|
216
|
+
}
|
|
217
|
+
headerLines.push("\r\n");
|
|
218
|
+
return Buffer.concat([Buffer.from(headerLines.join(""), "utf8"), value, Buffer.from("\r\n", "utf8")]);
|
|
219
|
+
}
|
|
220
|
+
function normalizeAttachmentResult(data) {
|
|
221
|
+
if (data.results) {
|
|
222
|
+
const first = data.results[0];
|
|
223
|
+
if (!first) {
|
|
224
|
+
throw new ConfluenceApiError("Attachment upload returned no results", 0, "attachment", JSON.stringify(data));
|
|
225
|
+
}
|
|
226
|
+
return first;
|
|
227
|
+
}
|
|
228
|
+
return data;
|
|
229
|
+
}
|
|
230
|
+
function normalizeBaseUrl(baseUrl) {
|
|
231
|
+
const trimmed = baseUrl.trim();
|
|
232
|
+
if (trimmed.length === 0) {
|
|
233
|
+
throw new Error("baseUrl must not be empty");
|
|
234
|
+
}
|
|
235
|
+
let url = trimmed;
|
|
236
|
+
while (url.endsWith("/")) {
|
|
237
|
+
url = url.slice(0, -1);
|
|
238
|
+
}
|
|
239
|
+
return url;
|
|
240
|
+
}
|
|
241
|
+
function describeStatus(status) {
|
|
242
|
+
switch (status) {
|
|
243
|
+
case STATUS_CODES.UNAUTHORIZED:
|
|
244
|
+
return "Authentication failed (check username/apiToken)";
|
|
245
|
+
case STATUS_CODES.FORBIDDEN:
|
|
246
|
+
return "Permission denied";
|
|
247
|
+
case STATUS_CODES.NOT_FOUND:
|
|
248
|
+
return "Resource not found";
|
|
249
|
+
case STATUS_CODES.CONFLICT:
|
|
250
|
+
return "Version conflict (page was updated concurrently)";
|
|
251
|
+
default:
|
|
252
|
+
if (status >= STATUS_CODES.BAD_REQUEST && status < 500) {
|
|
253
|
+
return `Client error (${status})`;
|
|
254
|
+
}
|
|
255
|
+
if (status >= 500) {
|
|
256
|
+
return `Server error (${status})`;
|
|
257
|
+
}
|
|
258
|
+
return `Unexpected status (${status})`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/files.ts
|
|
263
|
+
import { readdir, stat } from "fs/promises";
|
|
264
|
+
import { join, relative, sep, normalize } from "path";
|
|
265
|
+
var MARKDOWN_EXT = ".md";
|
|
266
|
+
var MAX_DEPTH = 32;
|
|
267
|
+
async function readDocTree(root, depth = 0) {
|
|
268
|
+
const normalizedRoot = normalize(root);
|
|
269
|
+
const info = await stat(normalizedRoot);
|
|
270
|
+
if (!info.isDirectory()) {
|
|
271
|
+
throw new Error(`Not a directory: ${normalizedRoot}`);
|
|
272
|
+
}
|
|
273
|
+
if (depth > MAX_DEPTH) {
|
|
274
|
+
throw new Error(`Max directory depth exceeded under ${normalizedRoot}`);
|
|
275
|
+
}
|
|
276
|
+
const entries = [];
|
|
277
|
+
await walk(normalizedRoot, normalizedRoot, entries, depth);
|
|
278
|
+
entries.sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
|
|
279
|
+
return { entries };
|
|
280
|
+
}
|
|
281
|
+
async function walk(dir, root, out, depth) {
|
|
282
|
+
if (depth > MAX_DEPTH) {
|
|
283
|
+
throw new Error(`Max directory depth exceeded under ${root}`);
|
|
284
|
+
}
|
|
285
|
+
const names = await readdir(dir, { withFileTypes: true });
|
|
286
|
+
for (const entry of names) {
|
|
287
|
+
if (entry.name.startsWith(".")) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const full = join(dir, entry.name);
|
|
291
|
+
if (entry.isDirectory()) {
|
|
292
|
+
await walk(full, root, out, depth + 1);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (entry.isFile() && entry.name.endsWith(MARKDOWN_EXT)) {
|
|
296
|
+
const rel = relative(root, full).split(sep).join("/");
|
|
297
|
+
out.push({ segments: rel.split("/"), absolute: full });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function isMarkdownName(name) {
|
|
302
|
+
const lower = name.toLowerCase();
|
|
303
|
+
return lower.length > MARKDOWN_EXT.length && lower.endsWith(MARKDOWN_EXT);
|
|
304
|
+
}
|
|
305
|
+
function titleFromSegment(segment) {
|
|
306
|
+
const dot = segment.lastIndexOf(".");
|
|
307
|
+
if (dot > 0) {
|
|
308
|
+
return segment.slice(0, dot);
|
|
309
|
+
}
|
|
310
|
+
return segment;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/markdown.ts
|
|
314
|
+
var STORAGE_LINE_BREAK = "<br />";
|
|
315
|
+
var LINE_BREAK_SENTINEL = "BR";
|
|
316
|
+
var PROTOCOL_BLOCKLIST = /^(?:javascript|data|file|vbscript):/i;
|
|
317
|
+
var AMP = "&";
|
|
318
|
+
var LT = "<";
|
|
319
|
+
var GT = ">";
|
|
320
|
+
var QUOT = """;
|
|
321
|
+
var APOS = "'";
|
|
322
|
+
function markdownToStorage(markdown) {
|
|
323
|
+
const lines = markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
324
|
+
const out = [];
|
|
325
|
+
let i = 0;
|
|
326
|
+
while (i < lines.length) {
|
|
327
|
+
const line = lines[i];
|
|
328
|
+
if (line === void 0 || line === "") {
|
|
329
|
+
i += 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (/^\s{0,3}#{1,6}\s/.test(line)) {
|
|
333
|
+
out.push(renderHeading(line));
|
|
334
|
+
i += 1;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (/^\s{0,3}```/.test(line)) {
|
|
338
|
+
const fence = line;
|
|
339
|
+
const lang = fence.trim().slice(3).trim();
|
|
340
|
+
const buf = [];
|
|
341
|
+
i += 1;
|
|
342
|
+
while (i < lines.length && !/^\s{0,3}```/.test(lines[i] ?? "")) {
|
|
343
|
+
buf.push(lines[i] ?? "");
|
|
344
|
+
i += 1;
|
|
345
|
+
}
|
|
346
|
+
i += 1;
|
|
347
|
+
out.push(renderCodeBlock(buf.join("\n"), lang));
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (/^\s{0,3}(?:-|\*|\+)\s+/.test(line) || /^\s{0,3}\d+\.\s+/.test(line)) {
|
|
351
|
+
const listLines = [];
|
|
352
|
+
while (i < lines.length && typeof lines[i] === "string" && (/^\s{0,3}(?:-|\*|\+)\s+/.test(lines[i]) || /^\s{0,3}\d+\.\s+/.test(lines[i]) || lines[i].trim() === "")) {
|
|
353
|
+
if (lines[i].trim() === "" && isLikelyListTerminator(lines, i)) {
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
listLines.push(lines[i]);
|
|
357
|
+
i += 1;
|
|
358
|
+
}
|
|
359
|
+
out.push(renderList(listLines));
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (/^\s{0,3}>/.test(line)) {
|
|
363
|
+
const quoteLines = [];
|
|
364
|
+
while (i < lines.length && typeof lines[i] === "string" && /^\s{0,3}>/.test(lines[i])) {
|
|
365
|
+
quoteLines.push(lines[i].replace(/^\s{0,3}>\s?/, ""));
|
|
366
|
+
i += 1;
|
|
367
|
+
}
|
|
368
|
+
out.push(`<blockquote>${renderInline(quoteLines.join("\n"))}</blockquote>`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (/^\s{0,3}---+\s*$/.test(line) || /^\s{0,3}\*\*\*+\s*$/.test(line)) {
|
|
372
|
+
out.push("<hr />");
|
|
373
|
+
i += 1;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const para = [];
|
|
377
|
+
while (i < lines.length && typeof lines[i] === "string" && lines[i].trim() !== "" && !/^\s{0,3}#{1,6}\s/.test(lines[i]) && !/^\s{0,3}```/.test(lines[i]) && !/^\s{0,3}(?:-|\*|\+)\s+/.test(lines[i]) && !/^\s{0,3}\d+\.\s+/.test(lines[i]) && !/^\s{0,3}>/.test(lines[i]) && !/^\s{0,3}---+\s*$/.test(lines[i]) && !/^\s{0,3}\*\*\*+\s*$/.test(lines[i])) {
|
|
378
|
+
para.push(lines[i]);
|
|
379
|
+
i += 1;
|
|
380
|
+
}
|
|
381
|
+
const renderedPara = renderInline(para.join(LINE_BREAK_SENTINEL));
|
|
382
|
+
out.push(`<p>${renderedPara.split(LINE_BREAK_SENTINEL).join(STORAGE_LINE_BREAK)}</p>`);
|
|
383
|
+
}
|
|
384
|
+
return { html: out.join("\n") };
|
|
385
|
+
}
|
|
386
|
+
function isLikelyListTerminator(lines, currentIndex) {
|
|
387
|
+
for (let k = currentIndex + 1; k < lines.length; k += 1) {
|
|
388
|
+
const next = lines[k];
|
|
389
|
+
if (next === void 0 || next.trim() === "") {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
return !/^\s{0,3}(?:-|\*|\+)\s+/.test(next) && !/^\s{0,3}\d+\.\s+/.test(next);
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
function renderHeading(line) {
|
|
397
|
+
const match = /^(\s{0,3})(#{1,6})\s+(.*)$/.exec(line);
|
|
398
|
+
if (!match) {
|
|
399
|
+
return "";
|
|
400
|
+
}
|
|
401
|
+
const hashes = match[2];
|
|
402
|
+
const level = hashes ? hashes.length : 1;
|
|
403
|
+
const text = match[3];
|
|
404
|
+
return `<h${level}>${renderInline(text)}</h${level}>`;
|
|
405
|
+
}
|
|
406
|
+
function renderCodeBlock(code, _lang) {
|
|
407
|
+
const lang = _lang && /^[a-zA-Z0-9+-]+$/.test(_lang) ? _lang : "none";
|
|
408
|
+
const titleAttr = escapeXmlAttribute(lang);
|
|
409
|
+
return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${titleAttr}</ac:parameter><ac:plain-text-body><![CDATA[${escapeCdataTerminator(code)}]]></ac:plain-text-body></ac:structured-macro>`;
|
|
410
|
+
}
|
|
411
|
+
function escapeCdataTerminator(text) {
|
|
412
|
+
return text.replace(/]]>/g, "]]]]><![CDATA[>");
|
|
413
|
+
}
|
|
414
|
+
function renderList(listLines) {
|
|
415
|
+
const items = [];
|
|
416
|
+
let ordered = false;
|
|
417
|
+
for (const raw of listLines) {
|
|
418
|
+
if (raw.trim() === "") {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const ulMatch = /^(\s{0,3})(?:-|\*|\+)\s+(.*)$/.exec(raw);
|
|
422
|
+
const olMatch = /^(\s{0,3})(\d+)\.\s+(.*)$/.exec(raw);
|
|
423
|
+
if (olMatch) {
|
|
424
|
+
ordered = true;
|
|
425
|
+
items.push(olMatch[3] ?? "");
|
|
426
|
+
} else if (ulMatch) {
|
|
427
|
+
items.push(ulMatch[2] ?? "");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const tag = ordered ? "ol" : "ul";
|
|
431
|
+
const body = items.map((item) => `<li>${renderInline(item)}</li>`).join("");
|
|
432
|
+
return `<${tag}>${body}</${tag}>`;
|
|
433
|
+
}
|
|
434
|
+
function renderInline(text) {
|
|
435
|
+
let s = text;
|
|
436
|
+
s = escapeHtml(s);
|
|
437
|
+
s = applyImages(s);
|
|
438
|
+
s = applyLinks(s);
|
|
439
|
+
s = applyStrong(s);
|
|
440
|
+
s = applyInlineCode(s);
|
|
441
|
+
return s;
|
|
442
|
+
}
|
|
443
|
+
function applyLinks(text) {
|
|
444
|
+
return replaceBalancedSyntax(text, /\[([^\]]+)\]\(/g, (label, url) => {
|
|
445
|
+
if (PROTOCOL_BLOCKLIST.test(url)) {
|
|
446
|
+
return label;
|
|
447
|
+
}
|
|
448
|
+
return '<a href="' + escapeXmlAttribute(url) + '">' + label + "</a>";
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function applyImages(text) {
|
|
452
|
+
return replaceBalancedSyntax(text, /!\[([^\]]*)\]\(/g, (alt, src) => {
|
|
453
|
+
if (PROTOCOL_BLOCKLIST.test(src)) {
|
|
454
|
+
return alt;
|
|
455
|
+
}
|
|
456
|
+
if (isRemoteUrl(src)) {
|
|
457
|
+
return '<ac:image><ri:url ri:value="' + escapeXmlAttribute(src) + '" /></ac:image>';
|
|
458
|
+
}
|
|
459
|
+
return '<ac:image data-local-src="' + escapeXmlAttribute(src) + '"></ac:image>';
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
function replaceBalancedSyntax(text, openerRe, render) {
|
|
463
|
+
let out = "";
|
|
464
|
+
let cursor = 0;
|
|
465
|
+
let m;
|
|
466
|
+
const re = new RegExp(openerRe.source, "g");
|
|
467
|
+
while ((m = re.exec(text)) !== null) {
|
|
468
|
+
out += text.slice(cursor, m.index);
|
|
469
|
+
const captured = m[1] ?? "";
|
|
470
|
+
const after = text.slice(m.index + m[0].length);
|
|
471
|
+
const scan = scanBalancedUrl(after);
|
|
472
|
+
if (scan === null) {
|
|
473
|
+
out += m[0];
|
|
474
|
+
cursor = m.index + m[0].length;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
out += render(captured, scan.url);
|
|
478
|
+
cursor = m.index + m[0].length + scan.consumed;
|
|
479
|
+
}
|
|
480
|
+
out += text.slice(cursor);
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
function scanBalancedUrl(rest) {
|
|
484
|
+
let depth = 0;
|
|
485
|
+
let i = 0;
|
|
486
|
+
for (; i < rest.length; i += 1) {
|
|
487
|
+
const ch = rest[i];
|
|
488
|
+
if (ch === "(") {
|
|
489
|
+
depth += 1;
|
|
490
|
+
} else if (ch === ")") {
|
|
491
|
+
if (depth === 0) {
|
|
492
|
+
const url = rest.slice(0, i);
|
|
493
|
+
if (url.length === 0) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
return { url, consumed: i + 1 };
|
|
497
|
+
}
|
|
498
|
+
depth -= 1;
|
|
499
|
+
} else if (ch === " " || ch === " " || ch === "\n") {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
var LOCAL_IMAGE_PLACEHOLDER_RE = /<ac:image\s+data-local-src="([^"]*)"\s*><\/ac:image>/g;
|
|
506
|
+
function applyStrong(text) {
|
|
507
|
+
let s = text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
508
|
+
s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
509
|
+
s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
510
|
+
return s.replace(/_([^_]+)_/g, "<em>$1</em>");
|
|
511
|
+
}
|
|
512
|
+
function applyInlineCode(text) {
|
|
513
|
+
return text.replace(/`([^`]+)`/g, (_m, code) => `<code>${code}</code>`);
|
|
514
|
+
}
|
|
515
|
+
function isRemoteUrl(src) {
|
|
516
|
+
return /^(https?:)?\/\//i.test(src) || /^\/\//.test(src);
|
|
517
|
+
}
|
|
518
|
+
function escapeHtml(text) {
|
|
519
|
+
return text.replace(/&/g, AMP).replace(/</g, LT).replace(/>/g, GT).replace(/"/g, QUOT).replace(/'/g, APOS);
|
|
520
|
+
}
|
|
521
|
+
function escapeXmlAttribute(text) {
|
|
522
|
+
return text.replace(/&/g, AMP).replace(/"/g, QUOT).replace(/</g, LT).replace(/>/g, GT);
|
|
523
|
+
}
|
|
524
|
+
function escapeAttachmentFilename(filename) {
|
|
525
|
+
if (filename === "" || filename === "." || filename === "..") {
|
|
526
|
+
throw new Error("Invalid attachment filename");
|
|
527
|
+
}
|
|
528
|
+
const cleaned = filename.replace(/[/\\]/g, "_");
|
|
529
|
+
if (cleaned === "" || cleaned === "." || cleaned === "..") {
|
|
530
|
+
throw new Error("Invalid attachment filename");
|
|
531
|
+
}
|
|
532
|
+
return cleaned;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/attachments.ts
|
|
536
|
+
import { existsSync } from "fs";
|
|
537
|
+
import { normalize as normalize2, isAbsolute, resolve } from "path";
|
|
538
|
+
async function rewriteImagesToAttachments(html, pageId, client, options) {
|
|
539
|
+
const uploaded = [];
|
|
540
|
+
const resolved = await resolvePlaceholders(html, pageId, client, options, uploaded);
|
|
541
|
+
return { html: resolved, uploaded };
|
|
542
|
+
}
|
|
543
|
+
async function resolvePlaceholders(html, pageId, client, options, uploaded) {
|
|
544
|
+
const existing = await client.getAttachments(pageId);
|
|
545
|
+
const existingByName = /* @__PURE__ */ new Map();
|
|
546
|
+
for (const a of existing) {
|
|
547
|
+
const name = a.filename ?? a.title;
|
|
548
|
+
if (name) {
|
|
549
|
+
existingByName.set(name, a);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const collected = [];
|
|
553
|
+
const seen = /* @__PURE__ */ new Set();
|
|
554
|
+
let m;
|
|
555
|
+
LOCAL_IMAGE_PLACEHOLDER_RE.lastIndex = 0;
|
|
556
|
+
while ((m = LOCAL_IMAGE_PLACEHOLDER_RE.exec(html)) !== null) {
|
|
557
|
+
const rawSrc = decodePlaceholder(m[1] ?? "");
|
|
558
|
+
if (seen.has(rawSrc)) {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
seen.add(rawSrc);
|
|
562
|
+
const abs = resolveImageForUpload(options.markdownDir, rawSrc);
|
|
563
|
+
if (!existsSync(abs)) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
collected.push({ src: rawSrc, abs });
|
|
567
|
+
}
|
|
568
|
+
const srcToFilename = /* @__PURE__ */ new Map();
|
|
569
|
+
for (const { src, abs } of collected) {
|
|
570
|
+
const filename = escapeAttachmentFilename(basenameLocal(abs));
|
|
571
|
+
let attachment = existingByName.get(filename);
|
|
572
|
+
if (attachment && attachment.id) {
|
|
573
|
+
attachment = await client.updateAttachmentData(pageId, attachment.id, abs, `Updated via repo-toolkit-confluence`);
|
|
574
|
+
} else {
|
|
575
|
+
attachment = await client.uploadAttachment(pageId, abs, `Uploaded via repo-toolkit-confluence`);
|
|
576
|
+
}
|
|
577
|
+
uploaded.push({ src, attachment });
|
|
578
|
+
srcToFilename.set(src, filename);
|
|
579
|
+
}
|
|
580
|
+
return html.replace(LOCAL_IMAGE_PLACEHOLDER_RE, (full, encodedSrc) => {
|
|
581
|
+
const src = decodePlaceholder(encodedSrc);
|
|
582
|
+
const filename = srcToFilename.get(src);
|
|
583
|
+
if (!filename) {
|
|
584
|
+
return full;
|
|
585
|
+
}
|
|
586
|
+
return renderAttachmentMacro(filename);
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function renderAttachmentMacro(filename) {
|
|
590
|
+
const safe = escapeAttachmentFilename(filename);
|
|
591
|
+
return `<ac:image><ri:attachment ri:filename="${escapeXmlAttribute(safe)}" /></ac:image>`;
|
|
592
|
+
}
|
|
593
|
+
function resolveImageForUpload(markdownDir, src) {
|
|
594
|
+
if (isRemoteUrl(src)) {
|
|
595
|
+
throw new Error(`Remote image should not be uploaded: ${src}`);
|
|
596
|
+
}
|
|
597
|
+
if (isAbsolute(src)) {
|
|
598
|
+
return normalize2(src);
|
|
599
|
+
}
|
|
600
|
+
return normalize2(resolve(markdownDir, src));
|
|
601
|
+
}
|
|
602
|
+
function basenameLocal(absPath) {
|
|
603
|
+
const norm = normalize2(absPath);
|
|
604
|
+
const sep2 = norm.includes("\\") ? "\\" : "/";
|
|
605
|
+
const parts = norm.split(sep2);
|
|
606
|
+
const last = parts[parts.length - 1];
|
|
607
|
+
return last ?? norm;
|
|
608
|
+
}
|
|
609
|
+
function decodePlaceholder(value) {
|
|
610
|
+
const AMP2 = "&";
|
|
611
|
+
const QUOT2 = """;
|
|
612
|
+
const LT2 = "<";
|
|
613
|
+
const GT2 = ">";
|
|
614
|
+
return value.replace(new RegExp(AMP2, "g"), "&").replace(new RegExp(QUOT2, "g"), '"').replace(new RegExp(LT2, "g"), "<").replace(new RegExp(GT2, "g"), ">");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/index.ts
|
|
618
|
+
function resolveConfluenceSyncPlan(options = {}) {
|
|
619
|
+
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
620
|
+
const folder = resolveInputPath(cwd, options.folder ?? "");
|
|
621
|
+
if (!options.folder) {
|
|
622
|
+
throw new Error("folder is required");
|
|
623
|
+
}
|
|
624
|
+
if (!options.dryRun) {
|
|
625
|
+
if (!options.username) {
|
|
626
|
+
throw new Error("username is required");
|
|
627
|
+
}
|
|
628
|
+
if (!options.apiToken) {
|
|
629
|
+
throw new Error("apiToken is required");
|
|
630
|
+
}
|
|
631
|
+
if (!options.baseUrl) {
|
|
632
|
+
throw new Error("baseUrl is required");
|
|
633
|
+
}
|
|
634
|
+
if (!options.spaceKey) {
|
|
635
|
+
throw new Error("spaceKey is required");
|
|
636
|
+
}
|
|
637
|
+
if (!options.parentPageId) {
|
|
638
|
+
throw new Error("parentPageId is required");
|
|
639
|
+
}
|
|
640
|
+
if (!/^[0-9]+$/.test(options.parentPageId)) {
|
|
641
|
+
throw new Error(`parentPageId must be numeric, got: ${options.parentPageId}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
cwd,
|
|
646
|
+
folder,
|
|
647
|
+
username: options.username ?? "",
|
|
648
|
+
apiToken: options.apiToken ?? "",
|
|
649
|
+
baseUrl: options.baseUrl ?? "",
|
|
650
|
+
spaceKey: options.spaceKey ?? "",
|
|
651
|
+
parentPageId: options.parentPageId ?? "",
|
|
652
|
+
versionMessage: options.versionMessage ?? "Synced via repo-toolkit-confluence",
|
|
653
|
+
skipUnchanged: options.skipUnchanged ?? true,
|
|
654
|
+
dryRun: options.dryRun ?? false
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
async function syncConfluenceToDocs(options = {}) {
|
|
658
|
+
const plan = resolveConfluenceSyncPlan(options);
|
|
659
|
+
const log = options.log ?? ((msg) => console.log(msg));
|
|
660
|
+
if (plan.dryRun) {
|
|
661
|
+
log("[dry-run] Walking documentation tree only.");
|
|
662
|
+
}
|
|
663
|
+
const tree = await readDocTree(plan.folder);
|
|
664
|
+
if (tree.entries.length === 0) {
|
|
665
|
+
log(`No markdown files found under ${plan.folder}`);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (plan.dryRun) {
|
|
669
|
+
for (const entry of tree.entries) {
|
|
670
|
+
log(`[dry-run] would sync ${entry.segments.join("/")}`);
|
|
671
|
+
}
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const client = options.client ?? new ConfluenceClient({
|
|
675
|
+
baseUrl: plan.baseUrl,
|
|
676
|
+
username: plan.username,
|
|
677
|
+
apiToken: plan.apiToken
|
|
678
|
+
});
|
|
679
|
+
const spaceId = await client.getSpaceIdByKey(plan.spaceKey);
|
|
680
|
+
const cache = new PageTitleCache(spaceId, client);
|
|
681
|
+
for (const entry of tree.entries) {
|
|
682
|
+
await syncEntry(entry, plan, client, cache, log);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async function syncEntry(entry, plan, client, cache, log) {
|
|
686
|
+
const segments = entry.segments;
|
|
687
|
+
if (segments.length === 0) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
let currentParentId = plan.parentPageId;
|
|
691
|
+
for (let idx = 0; idx < segments.length; idx += 1) {
|
|
692
|
+
const isLast = idx === segments.length - 1;
|
|
693
|
+
const segment = segments[idx] ?? "";
|
|
694
|
+
if (isLast && isMarkdownName(segment)) {
|
|
695
|
+
const title = titleFromSegment(segment);
|
|
696
|
+
const page2 = await cache.findOrCreate(title, currentParentId);
|
|
697
|
+
const pageId = page2.id;
|
|
698
|
+
const markdown = readFileSync2(entry.absolute, "utf8");
|
|
699
|
+
const { html } = markdownToStorage(markdown);
|
|
700
|
+
const markdownDir = dirname(entry.absolute);
|
|
701
|
+
let body = html;
|
|
702
|
+
LOCAL_IMAGE_PLACEHOLDER_RE.lastIndex = 0;
|
|
703
|
+
if (LOCAL_IMAGE_PLACEHOLDER_RE.test(body)) {
|
|
704
|
+
const result = await rewriteImagesToAttachments(body, pageId, client, { markdownDir });
|
|
705
|
+
body = result.html;
|
|
706
|
+
}
|
|
707
|
+
const current = await client.getPage(pageId);
|
|
708
|
+
const currentBody = current.body?.storage?.value ?? "";
|
|
709
|
+
const nextVersion = (current.version?.number ?? 0) + 1;
|
|
710
|
+
if (plan.skipUnchanged && currentBody === body) {
|
|
711
|
+
log(`unchanged: ${segments.join("/")} (page ${pageId})`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
await client.updatePage({
|
|
715
|
+
id: pageId,
|
|
716
|
+
title,
|
|
717
|
+
body: { representation: "storage", value: body },
|
|
718
|
+
version: { number: nextVersion, message: plan.versionMessage }
|
|
719
|
+
});
|
|
720
|
+
log(`updated: ${segments.join("/")} (page ${pageId}, v${nextVersion})`);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (isMarkdownName(segment)) {
|
|
724
|
+
const title = titleFromSegment(segment);
|
|
725
|
+
const page2 = await cache.findOrCreate(title, currentParentId);
|
|
726
|
+
currentParentId = page2.id;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const page = await cache.findOrCreate(segment, currentParentId);
|
|
730
|
+
currentParentId = page.id;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
var PageTitleCache = class {
|
|
734
|
+
constructor(spaceId, client) {
|
|
735
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
736
|
+
this.spaceId = spaceId;
|
|
737
|
+
this.client = client;
|
|
738
|
+
}
|
|
739
|
+
async findOrCreate(title, parentId) {
|
|
740
|
+
const key = `${parentId}::${title}`;
|
|
741
|
+
const existing = this.cache.get(key);
|
|
742
|
+
if (existing) {
|
|
743
|
+
return existing;
|
|
744
|
+
}
|
|
745
|
+
let page = await this.client.getPageByTitle(this.spaceId, title);
|
|
746
|
+
if (!page) {
|
|
747
|
+
page = await this.client.createPage({
|
|
748
|
+
spaceId: this.spaceId,
|
|
749
|
+
title,
|
|
750
|
+
parentId,
|
|
751
|
+
body: { representation: "storage", value: "" }
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
const result = { id: page.id };
|
|
755
|
+
this.cache.set(key, result);
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
function resolveInputPath(baseDir, inputPath) {
|
|
760
|
+
if (isAbsolute2(inputPath)) {
|
|
761
|
+
return inputPath;
|
|
762
|
+
}
|
|
763
|
+
return resolve2(baseDir, inputPath);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/cli.ts
|
|
767
|
+
var SPECS = [
|
|
768
|
+
{ name: "config" },
|
|
769
|
+
{ name: "cwd" },
|
|
770
|
+
{ name: "folder" },
|
|
771
|
+
{ name: "username" },
|
|
772
|
+
{ name: "api-token", aliases: ["password"] },
|
|
773
|
+
{ name: "confluence-base-url", aliases: ["base-url"] },
|
|
774
|
+
{ name: "space-key" },
|
|
775
|
+
{ name: "parent-page-id" },
|
|
776
|
+
{ name: "version-message" },
|
|
777
|
+
{ name: "skip-unchanged", boolean: true, negatable: true },
|
|
778
|
+
{ name: "dry-run", boolean: true },
|
|
779
|
+
INTERACTIVE_FLAG
|
|
780
|
+
];
|
|
781
|
+
function printHelp() {
|
|
782
|
+
console.log(`repo-toolkit-confluence
|
|
783
|
+
|
|
784
|
+
Usage:
|
|
785
|
+
repo-toolkit-confluence [options]
|
|
786
|
+
|
|
787
|
+
Synchronizes a folder of markdown documentation to Confluence pages and
|
|
788
|
+
attachments. When run with no flags, reads configuration from the GitHub
|
|
789
|
+
Action INPUT_* environment variables (folder, username, api-token,
|
|
790
|
+
confluence-base-url, space-key, parent-page-id).
|
|
791
|
+
|
|
792
|
+
Options:
|
|
793
|
+
--config <path> Config file (JSON, .mjs, or .cjs default export)
|
|
794
|
+
--cwd <path> Working directory (default: process.cwd())
|
|
795
|
+
--folder <path> Folder containing the documentation to publish (required)
|
|
796
|
+
--username <value> Confluence username or email (required)
|
|
797
|
+
--api-token <value> Confluence API token (required). Alias: --password
|
|
798
|
+
--confluence-base-url <url> Confluence URL with /wiki (required). Alias: --base-url
|
|
799
|
+
--space-key <key> Confluence space key (required). Resolved to a spaceId via the API
|
|
800
|
+
--parent-page-id <id> Numeric page id under which docs will be published (required)
|
|
801
|
+
--version-message <text> Commit message appended to every page/attachment PUT
|
|
802
|
+
--skip-unchanged Skip pages whose body is unchanged (default: true)
|
|
803
|
+
--no-skip-unchanged Re-upload every page even when unchanged
|
|
804
|
+
--dry-run Walk the doc tree and print the plan without API calls
|
|
805
|
+
-i, --interactive (reserved; not currently interactive)
|
|
806
|
+
-h, --help Show this help message
|
|
807
|
+
`);
|
|
808
|
+
}
|
|
809
|
+
var ENV_INPUT_MAP = [
|
|
810
|
+
["INPUT_FOLDER", "folder"],
|
|
811
|
+
["INPUT_USERNAME", "username"],
|
|
812
|
+
["INPUT_API-TOKEN", "apiToken"],
|
|
813
|
+
["INPUT_PASSWORD", "apiToken"],
|
|
814
|
+
["INPUT_CONFLUENCE-BASE-URL", "baseUrl"],
|
|
815
|
+
["INPUT_SPACE-KEY", "spaceKey"],
|
|
816
|
+
["INPUT_PARENT-PAGE-ID", "parentPageId"],
|
|
817
|
+
["INPUT_VERSION-MESSAGE", "versionMessage"]
|
|
818
|
+
];
|
|
819
|
+
function buildOptions(result) {
|
|
820
|
+
if (!result) {
|
|
821
|
+
return {};
|
|
822
|
+
}
|
|
823
|
+
const { values, repeat: _repeat } = result;
|
|
824
|
+
void _repeat;
|
|
825
|
+
const options = {};
|
|
826
|
+
if (values.cwd) options.cwd = values.cwd;
|
|
827
|
+
if (values.folder) options.folder = values.folder;
|
|
828
|
+
if (values.username) options.username = values.username;
|
|
829
|
+
if (values["api-token"]) options.apiToken = values["api-token"];
|
|
830
|
+
if (values["password"]) options.apiToken = values["password"];
|
|
831
|
+
if (values["confluence-base-url"]) options.baseUrl = values["confluence-base-url"];
|
|
832
|
+
if (values["base-url"]) options.baseUrl = values["base-url"];
|
|
833
|
+
if (values["space-key"]) options.spaceKey = values["space-key"];
|
|
834
|
+
if (values["parent-page-id"]) options.parentPageId = values["parent-page-id"];
|
|
835
|
+
if (values["version-message"]) options.versionMessage = values["version-message"];
|
|
836
|
+
if (values["skip-unchanged"] !== void 0) options.skipUnchanged = values["skip-unchanged"] === "true";
|
|
837
|
+
if (values["dry-run"] !== void 0) options.dryRun = true;
|
|
838
|
+
return options;
|
|
839
|
+
}
|
|
840
|
+
function optionsFromEnv() {
|
|
841
|
+
const options = {};
|
|
842
|
+
for (const [envName, key] of ENV_INPUT_MAP) {
|
|
843
|
+
const value = process.env[envName];
|
|
844
|
+
if (typeof value === "string" && value.length > 0) {
|
|
845
|
+
options[key] = value;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return options;
|
|
849
|
+
}
|
|
850
|
+
async function main() {
|
|
851
|
+
const argv = process.argv.slice(2);
|
|
852
|
+
const hasFlags = argv.length > 0;
|
|
853
|
+
const result = parseFlags2(argv, SPECS);
|
|
854
|
+
if (!result) {
|
|
855
|
+
printHelp();
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const cliOptions = await resolveCliOptions2({
|
|
859
|
+
result,
|
|
860
|
+
buildOptions
|
|
861
|
+
});
|
|
862
|
+
const envOptions = hasFlags ? {} : optionsFromEnv();
|
|
863
|
+
const merged = { ...envOptions, ...cliOptions };
|
|
864
|
+
await syncConfluenceToDocs(merged);
|
|
865
|
+
}
|
|
866
|
+
main().catch((error) => {
|
|
867
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
868
|
+
console.error(message);
|
|
869
|
+
process.exitCode = 1;
|
|
870
|
+
});
|