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.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/attachments/attachment.d.ts +39 -0
- package/dist/attachments/attachment.js +83 -0
- package/dist/attachments/hash.d.ts +2 -0
- package/dist/attachments/hash.js +6 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +85 -0
- package/dist/client/client.d.ts +128 -0
- package/dist/client/client.js +311 -0
- package/dist/client/config.d.ts +42 -0
- package/dist/client/config.js +54 -0
- package/dist/csv.d.ts +18 -0
- package/dist/csv.js +86 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +20 -0
- package/dist/macros/builder.d.ts +30 -0
- package/dist/macros/builder.js +58 -0
- package/dist/macros/index.d.ts +44 -0
- package/dist/macros/index.js +50 -0
- package/dist/macros/plugins/core.d.ts +49 -0
- package/dist/macros/plugins/core.js +214 -0
- package/dist/macros/plugins/table-filter.d.ts +35 -0
- package/dist/macros/plugins/table-filter.js +155 -0
- package/dist/macros/registry.d.ts +29 -0
- package/dist/macros/registry.js +111 -0
- package/dist/macros/types.d.ts +38 -0
- package/dist/macros/types.js +15 -0
- package/dist/macros/xml.d.ts +28 -0
- package/dist/macros/xml.js +52 -0
- package/dist/markdown/markdown.d.ts +36 -0
- package/dist/markdown/markdown.js +78 -0
- package/dist/markdown/render.d.ts +35 -0
- package/dist/markdown/render.js +80 -0
- package/dist/markdown/validate.d.ts +11 -0
- package/dist/markdown/validate.js +60 -0
- package/dist/pages/page.d.ts +104 -0
- package/dist/pages/page.js +234 -0
- package/dist/pages/tables.d.ts +72 -0
- package/dist/pages/tables.js +163 -0
- package/dist/publish/publish.d.ts +67 -0
- package/dist/publish/publish.js +164 -0
- package/dist/publish/runner.d.ts +36 -0
- package/dist/publish/runner.js +41 -0
- package/dist/wrapper.d.ts +21 -0
- package/dist/wrapper.js +36 -0
- 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
|
+
[](https://www.npmjs.com/package/confluence-md-sync)
|
|
4
|
+
[](https://github.com/hexstyle/confluence-md-sync/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](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
|
+
}
|
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
|
+
}
|