confluence-md-sync 0.1.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/dist/attachments/attachment.d.ts +39 -0
  4. package/dist/attachments/attachment.js +83 -0
  5. package/dist/attachments/hash.d.ts +2 -0
  6. package/dist/attachments/hash.js +6 -0
  7. package/dist/cli.d.ts +13 -0
  8. package/dist/cli.js +85 -0
  9. package/dist/client/client.d.ts +128 -0
  10. package/dist/client/client.js +311 -0
  11. package/dist/client/config.d.ts +42 -0
  12. package/dist/client/config.js +54 -0
  13. package/dist/csv.d.ts +18 -0
  14. package/dist/csv.js +86 -0
  15. package/dist/index.d.ts +13 -0
  16. package/dist/index.js +20 -0
  17. package/dist/macros/builder.d.ts +30 -0
  18. package/dist/macros/builder.js +58 -0
  19. package/dist/macros/index.d.ts +44 -0
  20. package/dist/macros/index.js +50 -0
  21. package/dist/macros/plugins/core.d.ts +49 -0
  22. package/dist/macros/plugins/core.js +214 -0
  23. package/dist/macros/plugins/table-filter.d.ts +35 -0
  24. package/dist/macros/plugins/table-filter.js +155 -0
  25. package/dist/macros/registry.d.ts +29 -0
  26. package/dist/macros/registry.js +111 -0
  27. package/dist/macros/types.d.ts +38 -0
  28. package/dist/macros/types.js +15 -0
  29. package/dist/macros/xml.d.ts +28 -0
  30. package/dist/macros/xml.js +52 -0
  31. package/dist/markdown/markdown.d.ts +36 -0
  32. package/dist/markdown/markdown.js +78 -0
  33. package/dist/markdown/render.d.ts +35 -0
  34. package/dist/markdown/render.js +80 -0
  35. package/dist/markdown/validate.d.ts +11 -0
  36. package/dist/markdown/validate.js +60 -0
  37. package/dist/pages/page.d.ts +104 -0
  38. package/dist/pages/page.js +234 -0
  39. package/dist/pages/tables.d.ts +72 -0
  40. package/dist/pages/tables.js +163 -0
  41. package/dist/publish/publish.d.ts +67 -0
  42. package/dist/publish/publish.js +164 -0
  43. package/dist/publish/runner.d.ts +36 -0
  44. package/dist/publish/runner.js +41 -0
  45. package/dist/wrapper.d.ts +21 -0
  46. package/dist/wrapper.js +36 -0
  47. package/package.json +69 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hexstyle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # confluence-md-sync
2
+
3
+ [![npm version](https://img.shields.io/npm/v/confluence-md-sync)](https://www.npmjs.com/package/confluence-md-sync)
4
+ [![CI](https://github.com/hexstyle/confluence-md-sync/actions/workflows/ci.yml/badge.svg)](https://github.com/hexstyle/confluence-md-sync/actions/workflows/ci.yml)
5
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![node >= 20.11](https://img.shields.io/badge/node-%E2%89%A5%2020.11-brightgreen)](package.json)
7
+
8
+ Docs-as-code for Confluence (Data Center & Cloud): publish Markdown, read
9
+ typed tables back, attach files — idempotently, with a **pluggable macro
10
+ system**. ESM-only, zero-config CI.
11
+
12
+ ```bash
13
+ npm install confluence-md-sync
14
+ ```
15
+
16
+ ## Highlights
17
+
18
+ - **No history spam** — rendered content is SHA-256-hashed into a content
19
+ property; identical re-publish skips the update, page version doesn't grow.
20
+ - **Attachment dedup** — uploads are tagged `sha256:<hash>`; unchanged files
21
+ are reused. `<file>.src-sha256` sidecars pin dedup to the *source* of
22
+ non-deterministic artifacts (e.g. PNGs rendered by a headless browser).
23
+ - **Fail before write** — placeholders, files and macro markers are validated
24
+ up front; Confluence is never touched on a broken input.
25
+ - **Pluggable macros** — built-in `core` + `table-filter` plugins, extend
26
+ with your own in a few lines.
27
+
28
+ ## Configuration
29
+
30
+ | Env var | Meaning |
31
+ | --- | --- |
32
+ | `CONFLUENCE_BASE_URL` | e.g. `https://confluence.example.com` |
33
+ | `CONFLUENCE_TOKEN` | PAT (Data Center) or API token (Cloud); `CONFLUENCE_PAT` also accepted |
34
+ | `CONFLUENCE_USERNAME` | required for `basic` auth (Cloud e-mail) |
35
+ | `CONFLUENCE_AUTH_TYPE` | `bearer` (default, DC) \| `basic` (Cloud) |
36
+
37
+ ```ts
38
+ import { loadConfigFromEnv } from 'confluence-md-sync';
39
+ const cfg = loadConfigFromEnv();
40
+ // or explicitly:
41
+ // const cfg = { baseUrl, token, username: 'me@example.com', authType: 'basic' };
42
+ ```
43
+
44
+ ## Publish a page
45
+
46
+ Markdown + three placeholder kinds:
47
+
48
+ ```markdown
49
+ # Отчёт за месяц
50
+
51
+ {{img:flow.png}} <!-- inline image from images[] -->
52
+ Исходник: {{file:data.csv}} <!-- attachment link from files[] -->
53
+
54
+ {{table:summary}} <!-- table injected from tables[] -->
55
+ ```
56
+
57
+ ```ts
58
+ import { publishPage, macros, renderMarkdownTable } from 'confluence-md-sync';
59
+
60
+ const summary = renderMarkdownTable(rows, [
61
+ { header: 'Проект', cell: (r) => r.name },
62
+ { header: 'Часы', cell: (r) => r.hours.toFixed(2), align: 'right' },
63
+ { header: 'Статус', cell: (r) => r.status },
64
+ ]);
65
+
66
+ await publishPage({
67
+ pageId: '123456789',
68
+ markdownPath: 'docs/report.md',
69
+ images: ['build/flow.png'],
70
+ files: ['build/data.csv'],
71
+ tables: [{
72
+ name: 'summary',
73
+ // sortable/filterable table with a totals row
74
+ markdown: macros.tableFilter(
75
+ macros.tableExcerpt(summary, 'summary'),
76
+ { totalrow: ',Sum,' },
77
+ ),
78
+ }],
79
+ labels: ['report', 'auto'],
80
+ versionMessage: 'automated publish',
81
+ }, cfg);
82
+ ```
83
+
84
+ More cases:
85
+
86
+ ```ts
87
+ // By space + title — page is looked up and created when missing
88
+ await publishPage({ spaceKey: 'DOCS', title: 'Monthly Report',
89
+ parentPageId: '123', markdown: reportMd }, cfg);
90
+
91
+ // Dry run — full render + validation, nothing written (great for PR checks)
92
+ const { storage } = await publishPage({ pageId, markdownPath, dryRun: true }, cfg);
93
+
94
+ // Inline content instead of a file
95
+ await publishPage({ pageId, markdown: '# Generated\n\ntext' }, cfg);
96
+ ```
97
+
98
+ ## Read pages and tables
99
+
100
+ ```ts
101
+ import { confluence } from 'confluence-md-sync';
102
+
103
+ const page = await confluence(cfg).readPage('987654321');
104
+
105
+ // by index (0 = first) or by table-excerpt macro name
106
+ const employees = page.getTable('employees').toType<Employee>((row, e) => {
107
+ e.fio = row['ФИО'];
108
+ e.email = row['Почта'];
109
+ });
110
+
111
+ const csv = await page.getAttachmentText('timesheet.csv');
112
+ await page.removeOldAttachmentVersions(); // keep latest version only
113
+ await page.addLabels(['hr']);
114
+ ```
115
+
116
+ CSV helpers (BOM detection, Windows-1251 fallback, `;`/`,` auto-detect):
117
+
118
+ ```ts
119
+ import { decodeText, readCsv, writeCsv } from 'confluence-md-sync/csv';
120
+ const rows = readCsv(decodeText(await page.getAttachment('data.csv')));
121
+ ```
122
+
123
+ ## Macros
124
+
125
+ Builders return `Markdown` with comment markers; markers become
126
+ `<ac:structured-macro>` after rendering (nested macros resolve inner-first):
127
+
128
+ ```ts
129
+ // containers
130
+ macros.expand(body, 'Details');
131
+ macros.note(body, 'Внимание'); // also: info, warning, tip
132
+ macros.panel(body, { title: 'Panel' });
133
+ macros.codeBlock(src, { language: 'sql', title: 'Query' });
134
+ macros.excerpt(body);
135
+
136
+ // bodyless
137
+ macros.toc({ maxLevel: '3' });
138
+ macros.status('Green', 'ON TRACK');
139
+ macros.jiraIssue('PROJ-123');
140
+ macros.anchor('section-1');
141
+ macros.children({ depth: '2' });
142
+
143
+ // cross-page includes
144
+ macros.includePage('Page Title', 'DOCS');
145
+ macros.excerptInclude('Source Page');
146
+ macros.tableExcerptInclude('name', 'Source Page');
147
+
148
+ // Table Filter and Charts app
149
+ macros.tableExcerpt(md, 'name', /* hide */ true);
150
+ macros.tableFilter(md, { totalrow: ',,Sum' });
151
+ ```
152
+
153
+ Or write markers by hand right in Markdown:
154
+
155
+ ```markdown
156
+ <!-- MACRO:start:expand:title=Детали -->
157
+ Any **markdown**, including nested macros.
158
+ <!-- MACRO:end:expand -->
159
+ ```
160
+
161
+ ### Custom macro plugin
162
+
163
+ ```ts
164
+ import { MacroRegistry, coreMacrosPlugin, tableFilterPlugin,
165
+ structuredMacro } from 'confluence-md-sync';
166
+
167
+ const registry = new MacroRegistry()
168
+ .use(coreMacrosPlugin)
169
+ .use(tableFilterPlugin)
170
+ .register('roadmap', (ctx) =>
171
+ structuredMacro('roadmap', ctx.macroId, { params: ctx.params, richBody: ctx.body }));
172
+
173
+ await publishPage({ pageId, markdownPath, registry }, cfg);
174
+ ```
175
+
176
+ `structuredMacro()` handles escaping, rich vs plain (CDATA) bodies and `]]>`
177
+ splitting; `pageLinkValue()` builds `<ac:link><ri:page/>` parameter values.
178
+
179
+ ## CLI
180
+
181
+ ```bash
182
+ npx confluence-md-sync publish docs/page.md --page-id 123456789 \
183
+ --image build/flow.png --file build/data.csv --label docs
184
+
185
+ npx confluence-md-sync publish docs/page.md --space DOCS --title "Моя страница" --dry-run
186
+ ```
187
+
188
+ ## CI publish plans
189
+
190
+ ```ts
191
+ // docs/dev-process/publish.ts — run with `npx tsx`
192
+ import { runPublish } from 'confluence-md-sync';
193
+
194
+ await runPublish(import.meta.dirname, (here, build) => [
195
+ { pageId: '111', markdownPath: here('process.md'), images: [build('flow.png')] },
196
+ { pageId: '222', markdownPath: here('release.md') },
197
+ ]);
198
+ ```
199
+
200
+ ```yaml
201
+ # .github/workflows/docs.yml
202
+ - run: npx tsx docs/dev-process/publish.ts
203
+ env:
204
+ CONFLUENCE_BASE_URL: ${{ vars.CONFLUENCE_BASE_URL }}
205
+ CONFLUENCE_TOKEN: ${{ secrets.CONFLUENCE_TOKEN }}
206
+ ```
207
+
208
+ `here()` resolves next to the script, `build()` inside the artifacts dir; the
209
+ process exits 1 before any write on error.
210
+
211
+ ## Architecture
212
+
213
+ ```
214
+ src/
215
+ ├── client/ REST client + auth: pages, properties, attachments, labels, CQL
216
+ ├── markdown/ Markdown type, markdown-it → storage renderer, validation
217
+ ├── macros/ registry, builder, plugins (core, table-filter)
218
+ ├── attachments/ sha256 dedup, sidecar source hashes, version history
219
+ ├── pages/ Page/Table object model, table parse & render
220
+ ├── publish/ idempotent publish pipeline, runPublish plans
221
+ ├── wrapper.ts confluence() facade
222
+ ├── csv.ts CSV helpers (subpath export ./csv)
223
+ └── cli.ts confluence-md-sync CLI
224
+ ```
225
+
226
+ ## Roadmap
227
+
228
+ Cloud API v2 backend · page-tree sync from a directory · storage→Markdown
229
+ reverse conversion · Mermaid/PlantUML fences → images · fenced code →
230
+ `code` macro · HTTP retry/backoff · comments API · dry-run diff preview.
231
+
232
+ Issues and PRs welcome.
233
+
234
+ ## Development
235
+
236
+ ```bash
237
+ npm ci
238
+ npm run typecheck && npm test && npm run build
239
+ ```
240
+
241
+ Releasing (maintainers): `npm version minor && git push --follow-tags` —
242
+ the `v*` tag triggers the npm publish workflow (tag must match
243
+ `package.json` version).
244
+
245
+ ## License
246
+
247
+ [MIT](LICENSE)
@@ -0,0 +1,39 @@
1
+ import { ConfluenceClient, type AttachmentVersionData } from '../client/client.js';
2
+ export declare const SRC_SHA_SIDECAR_SUFFIX = ".src-sha256";
3
+ export interface EnsuredAttachment {
4
+ id: string;
5
+ filename: string;
6
+ reused: boolean;
7
+ hash: string;
8
+ /** Абсолютный URL скачивания аттача — подставляется в `<img>` / `<a>`. */
9
+ downloadUrl: string;
10
+ }
11
+ export interface AttachmentVersion {
12
+ version: number;
13
+ fileSize?: number;
14
+ author?: string;
15
+ }
16
+ /** Rich-объект аттача страницы Confluence с историей версий. */
17
+ export declare class Attachment {
18
+ readonly id: string;
19
+ readonly pageId: string;
20
+ readonly title: string;
21
+ readonly version: number;
22
+ readonly downloadUrl: string;
23
+ readonly versions: AttachmentVersion[];
24
+ constructor(id: string, pageId: string, title: string, version: number, downloadUrl: string, versions?: AttachmentVersion[]);
25
+ }
26
+ export declare function toAttachmentVersion(v: AttachmentVersionData): AttachmentVersion;
27
+ export declare class AttachmentService {
28
+ private client;
29
+ constructor(client: ConfluenceClient);
30
+ /**
31
+ * Загружает аттач (или переиспользует по SHA-256). Дедуп по тегу
32
+ * version.message = sha256:<hash>, чтобы повторные прогоны CI не плодили
33
+ * версии одинакового файла.
34
+ */
35
+ ensure(pageId: string, filePath: string): Promise<EnsuredAttachment>;
36
+ download(att: Attachment): Promise<Buffer>;
37
+ delete(att: Attachment): Promise<void>;
38
+ private downloadUrlOf;
39
+ }
@@ -0,0 +1,83 @@
1
+ import { basename } from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { fileSha256, HASH_TAG_PREFIX } from './hash.js';
4
+ // Sidecar с SHA-256 исходного файла (например, BPMN), из которого
5
+ // сгенерирован публикуемый аттач (PNG). Если рядом с filePath лежит
6
+ // `<filePath>.src-sha256`, используем содержимое sidecar как dedup-tag
7
+ // вместо SHA самого аттача. Это критично для PNG'шек от Chromium
8
+ // (anti-aliasing/fonts нестабильны между CI runs — SHA PNG скачет даже
9
+ // при том же исходнике), благодаря sidecar attachment не пересоздаётся
10
+ // повторно и Confluence не плодит лишние версии.
11
+ export const SRC_SHA_SIDECAR_SUFFIX = '.src-sha256';
12
+ function readDedupHash(filePath) {
13
+ const sidecar = filePath + SRC_SHA_SIDECAR_SUFFIX;
14
+ if (existsSync(sidecar)) {
15
+ return readFileSync(sidecar, 'utf-8').trim();
16
+ }
17
+ return fileSha256(filePath);
18
+ }
19
+ /** Rich-объект аттача страницы Confluence с историей версий. */
20
+ export class Attachment {
21
+ id;
22
+ pageId;
23
+ title;
24
+ version;
25
+ downloadUrl;
26
+ versions;
27
+ constructor(id, pageId, title, version, downloadUrl, versions = []) {
28
+ this.id = id;
29
+ this.pageId = pageId;
30
+ this.title = title;
31
+ this.version = version;
32
+ this.downloadUrl = downloadUrl;
33
+ this.versions = versions;
34
+ }
35
+ }
36
+ function versionNumberOf(v) {
37
+ return v.versionNumber ?? v.version ?? v.number ?? 0;
38
+ }
39
+ export function toAttachmentVersion(v) {
40
+ return { version: versionNumberOf(v), fileSize: v.fileSize, author: v.author?.fullName };
41
+ }
42
+ export class AttachmentService {
43
+ client;
44
+ constructor(client) {
45
+ this.client = client;
46
+ }
47
+ /**
48
+ * Загружает аттач (или переиспользует по SHA-256). Дедуп по тегу
49
+ * version.message = sha256:<hash>, чтобы повторные прогоны CI не плодили
50
+ * версии одинакового файла.
51
+ */
52
+ async ensure(pageId, filePath) {
53
+ const filename = basename(filePath);
54
+ // dedup-tag: SHA исходника из sidecar если есть, иначе SHA самого файла.
55
+ // См. readDedupHash наверху и SRC_SHA_SIDECAR_SUFFIX.
56
+ const hash = readDedupHash(filePath);
57
+ const tag = `${HASH_TAG_PREFIX}${hash}`;
58
+ const existing = await this.client.listAttachments(pageId, filename);
59
+ const matched = existing.find((a) => a.version?.message === tag);
60
+ if (matched) {
61
+ return { id: matched.id, filename, reused: true, hash, downloadUrl: this.downloadUrlOf(pageId, matched) };
62
+ }
63
+ const sameName = existing[0];
64
+ if (sameName) {
65
+ const updated = await this.client.updateAttachmentData(pageId, sameName.id, filePath, tag);
66
+ return { id: updated.id ?? sameName.id, filename, reused: false, hash, downloadUrl: this.downloadUrlOf(pageId, updated) };
67
+ }
68
+ const created = await this.client.createAttachment(pageId, filePath, tag);
69
+ return { id: created.id, filename, reused: false, hash, downloadUrl: this.downloadUrlOf(pageId, created) };
70
+ }
71
+ async download(att) {
72
+ return this.client.downloadAttachment(att.downloadUrl);
73
+ }
74
+ async delete(att) {
75
+ await this.client.deleteAttachment(att.id);
76
+ }
77
+ downloadUrlOf(pageId, attachment) {
78
+ const link = attachment._links?.download;
79
+ if (link)
80
+ return this.client.absoluteUrl(link);
81
+ return this.client.absoluteUrl(`/download/attachments/${pageId}/${encodeURIComponent(attachment.title)}`);
82
+ }
83
+ }
@@ -0,0 +1,2 @@
1
+ export declare const HASH_TAG_PREFIX = "sha256:";
2
+ export declare function fileSha256(filePath: string): string;
@@ -0,0 +1,6 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ export const HASH_TAG_PREFIX = 'sha256:';
4
+ export function fileSha256(filePath) {
5
+ return createHash('sha256').update(readFileSync(filePath)).digest('hex');
6
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Минимальный CLI:
4
+ *
5
+ * confluence-md-sync publish page.md --page-id 12345 \
6
+ * --image build/p1.png --file data.csv --label docs --dry-run
7
+ *
8
+ * confluence-md-sync publish page.md --space DOCS --title "My Page" --parent-id 777
9
+ *
10
+ * Конфиг — из env: CONFLUENCE_BASE_URL, CONFLUENCE_TOKEN (или CONFLUENCE_PAT),
11
+ * CONFLUENCE_USERNAME, CONFLUENCE_AUTH_TYPE (bearer|basic).
12
+ */
13
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Минимальный CLI:
4
+ *
5
+ * confluence-md-sync publish page.md --page-id 12345 \
6
+ * --image build/p1.png --file data.csv --label docs --dry-run
7
+ *
8
+ * confluence-md-sync publish page.md --space DOCS --title "My Page" --parent-id 777
9
+ *
10
+ * Конфиг — из env: CONFLUENCE_BASE_URL, CONFLUENCE_TOKEN (или CONFLUENCE_PAT),
11
+ * CONFLUENCE_USERNAME, CONFLUENCE_AUTH_TYPE (bearer|basic).
12
+ */
13
+ import { parseArgs } from 'node:util';
14
+ import { loadConfigFromEnv } from './client/config.js';
15
+ import { publishPage } from './publish/publish.js';
16
+ const HELP = `confluence-md-sync — publish Markdown to Confluence
17
+
18
+ Usage:
19
+ confluence-md-sync publish <markdown-file> [options]
20
+
21
+ Options:
22
+ --page-id <id> Target page ID
23
+ --space <key> Space key (with --title, when page ID is unknown)
24
+ --title <title> Page title (rename, or lookup/create key with --space)
25
+ --parent-id <id> Parent page for page creation
26
+ --image <path> Attach an image (repeatable), referenced as {{img:name.png}}
27
+ --file <path> Attach a file (repeatable), referenced as {{file:name.csv}}
28
+ --label <name> Ensure label on the page (repeatable)
29
+ --message <text> Page version comment
30
+ --no-create Fail instead of creating a missing page
31
+ --dry-run Render and validate without writing to Confluence
32
+ --help Show this help
33
+
34
+ Environment:
35
+ CONFLUENCE_BASE_URL e.g. https://confluence.example.com
36
+ CONFLUENCE_TOKEN PAT (Data Center) or API token (Cloud); CONFLUENCE_PAT also works
37
+ CONFLUENCE_USERNAME required for basic auth (Cloud e-mail)
38
+ CONFLUENCE_AUTH_TYPE bearer (default) | basic
39
+ `;
40
+ async function main() {
41
+ const { values, positionals } = parseArgs({
42
+ allowPositionals: true,
43
+ options: {
44
+ 'page-id': { type: 'string' },
45
+ space: { type: 'string' },
46
+ title: { type: 'string' },
47
+ 'parent-id': { type: 'string' },
48
+ image: { type: 'string', multiple: true },
49
+ file: { type: 'string', multiple: true },
50
+ label: { type: 'string', multiple: true },
51
+ message: { type: 'string' },
52
+ 'no-create': { type: 'boolean' },
53
+ 'dry-run': { type: 'boolean' },
54
+ help: { type: 'boolean' },
55
+ },
56
+ });
57
+ if (values.help || positionals.length === 0) {
58
+ console.log(HELP);
59
+ process.exit(values.help ? 0 : 1);
60
+ }
61
+ const [command, markdownPath] = positionals;
62
+ if (command !== 'publish' || !markdownPath) {
63
+ console.error(`Unknown command or missing file. See --help.`);
64
+ process.exit(1);
65
+ }
66
+ const cfg = loadConfigFromEnv();
67
+ const result = await publishPage({
68
+ pageId: values['page-id'],
69
+ spaceKey: values.space,
70
+ title: values.title,
71
+ parentPageId: values['parent-id'],
72
+ markdownPath,
73
+ images: values.image ?? [],
74
+ files: values.file ?? [],
75
+ labels: values.label,
76
+ versionMessage: values.message,
77
+ createIfMissing: values['no-create'] ? false : undefined,
78
+ dryRun: values['dry-run'],
79
+ }, cfg);
80
+ console.log(`[cli] ${result.updated ? 'published' : 'unchanged'}: page ${result.pageId} v${result.version}`);
81
+ }
82
+ main().catch((err) => {
83
+ console.error(`[cli] failed: ${err instanceof Error ? err.message : String(err)}`);
84
+ process.exit(1);
85
+ });
@@ -0,0 +1,128 @@
1
+ import { type ConfluenceConfig } from './config.js';
2
+ export interface ConfluencePage {
3
+ id: string;
4
+ title: string;
5
+ version: {
6
+ number: number;
7
+ };
8
+ }
9
+ export interface ConfluencePageStorage {
10
+ title: string;
11
+ storage: string;
12
+ version: number;
13
+ }
14
+ export interface ConfluenceAttachment {
15
+ id: string;
16
+ title: string;
17
+ version: {
18
+ number: number;
19
+ message?: string;
20
+ };
21
+ extensions?: {
22
+ fileSize?: number;
23
+ };
24
+ /**
25
+ * `_links.download` — относительный путь скачивания, который Confluence
26
+ * возвращает в ответе и который нужно подставлять в `<img src>` / `<a href>`
27
+ * после аплоада. Абсолютизируется через `client.absoluteUrl(...)`.
28
+ */
29
+ _links?: {
30
+ download?: string;
31
+ webui?: string;
32
+ };
33
+ }
34
+ /** Версия аттача из /rest/files/1.0/files/{id}/versions (поля гибкие). */
35
+ export interface AttachmentVersionData {
36
+ versionNumber?: number;
37
+ version?: number;
38
+ number?: number;
39
+ fileName?: string;
40
+ fileSize?: number;
41
+ author?: {
42
+ fullName?: string;
43
+ };
44
+ }
45
+ export interface ConfluenceLabel {
46
+ prefix: string;
47
+ name: string;
48
+ }
49
+ export interface CreatePageOptions {
50
+ spaceKey: string;
51
+ title: string;
52
+ parentId?: string;
53
+ /** Initial body in storage format. Defaults to an empty page. */
54
+ storage?: string;
55
+ }
56
+ export declare class ConfluenceApiError extends Error {
57
+ readonly status: number;
58
+ readonly statusText: string;
59
+ readonly detail: string;
60
+ constructor(message: string, status: number, statusText: string, detail: string);
61
+ }
62
+ export declare class ConfluenceClient {
63
+ private readonly auth;
64
+ readonly baseUrl: string;
65
+ constructor(cfg: ConfluenceConfig);
66
+ /** Превращает относительный путь из API (`/download/...`) в абсолютный URL. */
67
+ absoluteUrl(path: string): string;
68
+ private url;
69
+ private headers;
70
+ private parseError;
71
+ getPage(pageId: string): Promise<ConfluencePage>;
72
+ getPageStorage(pageId: string): Promise<ConfluencePageStorage>;
73
+ /** Finds a page by space key and exact title. Returns null if not found. */
74
+ getPageByTitle(spaceKey: string, title: string): Promise<ConfluencePage | null>;
75
+ createPage(opts: CreatePageOptions): Promise<ConfluencePage>;
76
+ updatePage(pageId: string, body: {
77
+ title: string;
78
+ version: number;
79
+ storage: string;
80
+ versionMessage?: string;
81
+ }): Promise<void>;
82
+ deletePage(pageId: string): Promise<void>;
83
+ /** Direct child pages of a page. */
84
+ getChildPages(pageId: string, limit?: number): Promise<ConfluencePage[]>;
85
+ /** CQL content search (e.g. `space = DOCS and type = page and title ~ "Report*"`). */
86
+ search(cql: string, limit?: number): Promise<ConfluencePage[]>;
87
+ getLabels(pageId: string): Promise<ConfluenceLabel[]>;
88
+ addLabels(pageId: string, labels: string[]): Promise<void>;
89
+ removeLabel(pageId: string, label: string): Promise<void>;
90
+ /**
91
+ * Content Property — произвольные key/value на странице вне body.
92
+ * Используется для content-hash (см. publish.ts): попытка хранить
93
+ * hash в body как HTML-comment не пережила нормализацию storage Confluence.
94
+ * Возвращает null, если property с таким ключом нет.
95
+ */
96
+ getContentProperty(pageId: string, key: string): Promise<{
97
+ value: unknown;
98
+ version: number;
99
+ } | null>;
100
+ /**
101
+ * Создаёт (если version null) или обновляет content property.
102
+ * При update Confluence требует следующий version-номер.
103
+ */
104
+ setContentProperty(pageId: string, key: string, value: unknown, currentVersion: number | null): Promise<void>;
105
+ listAttachments(pageId: string, filename?: string): Promise<ConfluenceAttachment[]>;
106
+ createAttachment(pageId: string, filePath: string, comment?: string): Promise<ConfluenceAttachment>;
107
+ /**
108
+ * Скачивает содержимое аттача по download-пути из `_links.download`
109
+ * (или абсолютному URL). Возвращает сырые байты — текстовая
110
+ * декодировка лежит на caller'е.
111
+ */
112
+ downloadAttachment(downloadPath: string): Promise<Buffer>;
113
+ updateAttachmentData(pageId: string, attachmentId: string, filePath: string, comment?: string): Promise<ConfluenceAttachment>;
114
+ /**
115
+ * Список версий аттача через Confluence Files API (Data Center only).
116
+ * Структура ответа гибкая — номер версии нормализуется потребителем
117
+ * (поле versionNumber / version / number).
118
+ */
119
+ getAttachmentVersions(attachmentId: string): Promise<AttachmentVersionData[]>;
120
+ /**
121
+ * Удаляет конкретную версию аттача (Data Center only). REST-аналога нет,
122
+ * идём через legacy-action; XSRF обходим заголовком X-Atlassian-Token:
123
+ * no-check (тот же приём работает для createAttachment).
124
+ */
125
+ removeAttachmentVersion(pageId: string, fileName: string, version: number): Promise<void>;
126
+ /** Удаляет аттач целиком (все версии). */
127
+ deleteAttachment(attachmentId: string): Promise<void>;
128
+ }