@bestend/confluence-cli 1.15.8 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +105 -0
- package/bin/confluence.js +46 -29
- package/lib/confluence-client.js +33 -33
- package/package.json +1 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Confluence CLI — 프로젝트 가이드
|
|
2
|
+
|
|
3
|
+
## 아키텍처
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
bin/
|
|
7
|
+
index.js # 엔트리포인트 (Node 버전 체크 → confluence.js 로딩)
|
|
8
|
+
confluence.js # Commander CLI 정의 (모든 커맨드 등록)
|
|
9
|
+
|
|
10
|
+
lib/
|
|
11
|
+
confluence-client.js # 핵심 클라이언트 (API 호출, 포맷 변환, 페이지 CRUD)
|
|
12
|
+
config.js # 설정 로드 (~/.confluence-cli/config.json)
|
|
13
|
+
analytics.js # 익명 사용 통계
|
|
14
|
+
|
|
15
|
+
tests/
|
|
16
|
+
confluence-client.test.js # Jest 단위 테스트
|
|
17
|
+
|
|
18
|
+
.github/workflows/
|
|
19
|
+
ci.yml # push/PR → lint + test (Node 18.x, 20.x)
|
|
20
|
+
publish.yml # tag push (v*) → npm publish with OIDC provenance
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 코드 스타일
|
|
24
|
+
|
|
25
|
+
- ESLint flat config (`eslint.config.js`)
|
|
26
|
+
- 들여쓰기: 2칸 스페이스
|
|
27
|
+
- 따옴표: 작은따옴표
|
|
28
|
+
- 세미콜론: 필수
|
|
29
|
+
- `no-unused-vars`: `_` 접두사 무시
|
|
30
|
+
|
|
31
|
+
## 핵심 변환 파이프라인
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Markdown → MarkdownIt.render() → HTML → htmlToConfluenceStorage() → Storage XML
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`htmlToConfluenceStorage()`는 MarkdownIt이 생성한 HTML 중 Confluence 전용 처리가 필요한 것만 변환:
|
|
38
|
+
- `<li>` → `<li><p>...</p></li>` (Confluence 필수)
|
|
39
|
+
- `<pre><code>` → `ac:structured-macro` code block with CDATA
|
|
40
|
+
- `<blockquote>` → info/warning/note 매크로
|
|
41
|
+
- `<th>`, `<td>` → `<p>` 래핑
|
|
42
|
+
- `<hr>` → `<hr />` (self-closing)
|
|
43
|
+
|
|
44
|
+
나머지 HTML 태그(h1-h6, p, strong, em, ul, ol, table 등)는 이미 Confluence storage와 호환되므로 변환하지 않음.
|
|
45
|
+
|
|
46
|
+
## 버전 관리
|
|
47
|
+
|
|
48
|
+
- `package.json`의 version은 직접 올리지 않아도 됨
|
|
49
|
+
- publish.yml이 git tag에서 버전을 추출해서 package.json을 자동 맞춤
|
|
50
|
+
- 단, 로컬 개발 시 혼동 방지를 위해 릴리스 전 맞춰두는 것을 권장
|
|
51
|
+
|
|
52
|
+
## 릴리스 절차
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 1. 변경 커밋 (Conventional Commits)
|
|
56
|
+
git add -A
|
|
57
|
+
git commit -m "feat: add --parent option to create command"
|
|
58
|
+
|
|
59
|
+
# 2. 태그 생성 (SemVer)
|
|
60
|
+
git tag v1.16.0
|
|
61
|
+
|
|
62
|
+
# 3. 푸시 (커밋 + 태그)
|
|
63
|
+
git push origin main --follow-tags
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
태그 push 시 자동으로:
|
|
67
|
+
1. `ci.yml` → lint + test (push to main 트리거)
|
|
68
|
+
2. `publish.yml` → npm publish with OIDC provenance (tag 트리거)
|
|
69
|
+
|
|
70
|
+
### 버전 결정 기준
|
|
71
|
+
|
|
72
|
+
| 변경 유형 | 예시 | 버전 |
|
|
73
|
+
|-----------|------|------|
|
|
74
|
+
| 새 명령어/옵션 추가 | `--parent` 옵션 | minor (x.Y.0) |
|
|
75
|
+
| 버그 수정, 내부 정리 | no-op regex 제거 | patch (x.y.Z) |
|
|
76
|
+
| 하위 호환 깨짐 | 명령어 인터페이스 변경 | major (X.0.0) |
|
|
77
|
+
|
|
78
|
+
### npm OIDC Trusted Publishing
|
|
79
|
+
|
|
80
|
+
publish.yml은 `id-token: write` 권한으로 npm OIDC provenance를 사용.
|
|
81
|
+
npm 토큰이 아닌 GitHub Actions OIDC로 인증하므로 시크릿 관리 불필요.
|
|
82
|
+
Node.js 24.x (npm 11.5.1+) 필요.
|
|
83
|
+
|
|
84
|
+
## 테스트
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm test # Jest 단위 테스트 (40개)
|
|
88
|
+
npm run lint # ESLint
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
테스트는 `axios-mock-adapter`로 HTTP 모킹. 실제 Confluence 인스턴스 불필요.
|
|
92
|
+
|
|
93
|
+
## agent-rules 연동
|
|
94
|
+
|
|
95
|
+
이 CLI는 [mona/agent-rules](https://oss.navercorp.com/mona/agent-rules)의 `confluence-cli` 스킬로 사용됨:
|
|
96
|
+
- 스킬 문서: `sources/skills/confluence-cli/SKILL.md`
|
|
97
|
+
- 설치: `npm install -g @bestend/confluence-cli@latest`
|
|
98
|
+
|
|
99
|
+
CLI 버전 릴리스 후 agent-rules의 SKILL.md `metadata.version`도 맞춰 업데이트할 것.
|
|
100
|
+
|
|
101
|
+
## 금지 사항
|
|
102
|
+
|
|
103
|
+
- `htmlToConfluenceStorage()`에 no-op regex 추가 금지 (X→X 변환은 의미 없음)
|
|
104
|
+
- 전역 entity unescape (`<`→`<` 등) 금지 — code block 내부는 이미 별도 디코딩됨
|
|
105
|
+
- OIDC publish 설정 변경 시 Node.js 24.x 이상 유지 필수
|
package/bin/confluence.js
CHANGED
|
@@ -141,11 +141,12 @@ program
|
|
|
141
141
|
|
|
142
142
|
// Create command
|
|
143
143
|
program
|
|
144
|
-
.command('create <title>
|
|
144
|
+
.command('create <title> [spaceKey]')
|
|
145
145
|
.description('Create a new Confluence page')
|
|
146
146
|
.option('-f, --file <file>', 'Read content from file')
|
|
147
147
|
.option('-c, --content <content>', 'Page content as string')
|
|
148
148
|
.option('--format <format>', 'Content format (storage, html, markdown)', 'storage')
|
|
149
|
+
.option('--parent <parentId>', 'Parent page ID (creates page as child of this page)')
|
|
149
150
|
.option('--validate-storage', 'Validate storage content (XML well-formed) before sending')
|
|
150
151
|
.option('--no-sanitize-storage', 'Disable auto-escaping raw ampersands in storage format')
|
|
151
152
|
.action(async (title, spaceKey, options) => {
|
|
@@ -168,17 +169,33 @@ program
|
|
|
168
169
|
throw new Error('Either --file or --content option is required');
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
172
|
+
let result;
|
|
173
|
+
if (options.parent) {
|
|
174
|
+
const parentInfo = await client.getPageInfo(options.parent);
|
|
175
|
+
const derivedSpaceKey = parentInfo.space.key;
|
|
176
|
+
result = await client.createChildPage(title, derivedSpaceKey, options.parent, content, options.format, {
|
|
177
|
+
validateStorage: options.validateStorage,
|
|
178
|
+
sanitizeStorage: options.sanitizeStorage
|
|
179
|
+
});
|
|
180
|
+
console.log(chalk.green('✅ Page created successfully!'));
|
|
181
|
+
console.log(`Title: ${chalk.blue(result.title)}`);
|
|
182
|
+
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
183
|
+
console.log(`Parent: ${chalk.blue(parentInfo.title)} (${options.parent})`);
|
|
184
|
+
} else {
|
|
185
|
+
if (!spaceKey) {
|
|
186
|
+
throw new Error('Space key is required when --parent is not specified');
|
|
187
|
+
}
|
|
188
|
+
result = await client.createPage(title, spaceKey, content, options.format, {
|
|
189
|
+
validateStorage: options.validateStorage,
|
|
190
|
+
sanitizeStorage: options.sanitizeStorage
|
|
191
|
+
});
|
|
192
|
+
console.log(chalk.green('✅ Page created successfully!'));
|
|
193
|
+
console.log(`Title: ${chalk.blue(result.title)}`);
|
|
194
|
+
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
195
|
+
}
|
|
179
196
|
console.log(`Space: ${chalk.blue(result.space.name)} (${result.space.key})`);
|
|
180
|
-
console.log(`URL: ${chalk.gray(
|
|
181
|
-
|
|
197
|
+
console.log(`URL: ${chalk.gray(client.pageUrl(result._links.webui))}`);
|
|
198
|
+
|
|
182
199
|
analytics.track('create', true);
|
|
183
200
|
} catch (error) {
|
|
184
201
|
analytics.track('create', false);
|
|
@@ -232,8 +249,8 @@ program
|
|
|
232
249
|
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
233
250
|
console.log(`Parent: ${chalk.blue(parentInfo.title)} (${parentId})`);
|
|
234
251
|
console.log(`Space: ${chalk.blue(result.space.name)} (${result.space.key})`);
|
|
235
|
-
console.log(`URL: ${chalk.gray(
|
|
236
|
-
|
|
252
|
+
console.log(`URL: ${chalk.gray(client.pageUrl(result._links.webui))}`);
|
|
253
|
+
|
|
237
254
|
analytics.track('create_child', true);
|
|
238
255
|
} catch (error) {
|
|
239
256
|
analytics.track('create_child', false);
|
|
@@ -284,8 +301,8 @@ program
|
|
|
284
301
|
console.log(`Title: ${chalk.blue(result.title)}`);
|
|
285
302
|
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
286
303
|
console.log(`Version: ${chalk.blue(result.version.number)}`);
|
|
287
|
-
console.log(`URL: ${chalk.gray(
|
|
288
|
-
|
|
304
|
+
console.log(`URL: ${chalk.gray(client.pageUrl(result._links.webui))}`);
|
|
305
|
+
|
|
289
306
|
analytics.track('update', true);
|
|
290
307
|
} catch (error) {
|
|
291
308
|
analytics.track('update', false);
|
|
@@ -390,8 +407,8 @@ program
|
|
|
390
407
|
console.log(`Title: ${chalk.green(pageInfo.title)}`);
|
|
391
408
|
console.log(`ID: ${chalk.green(pageInfo.id)}`);
|
|
392
409
|
console.log(`Space: ${chalk.green(pageInfo.space.name)} (${pageInfo.space.key})`);
|
|
393
|
-
console.log(`URL: ${chalk.gray(
|
|
394
|
-
|
|
410
|
+
console.log(`URL: ${chalk.gray(client.pageUrl(pageInfo.url))}`);
|
|
411
|
+
|
|
395
412
|
analytics.track('find', true);
|
|
396
413
|
} catch (error) {
|
|
397
414
|
analytics.track('find', false);
|
|
@@ -1067,7 +1084,7 @@ program
|
|
|
1067
1084
|
console.log(` - ...and ${result.failures.length - 10} more`);
|
|
1068
1085
|
}
|
|
1069
1086
|
}
|
|
1070
|
-
console.log(`URL: ${chalk.gray(
|
|
1087
|
+
console.log(`URL: ${chalk.gray(client.pageUrl(result.rootPage._links.webui))}`);
|
|
1071
1088
|
if (options.failOnError && result.failures?.length) {
|
|
1072
1089
|
analytics.track('copy_tree', false);
|
|
1073
1090
|
console.error(chalk.red('Completed with failures and --fail-on-error is set.'));
|
|
@@ -1129,7 +1146,7 @@ program
|
|
|
1129
1146
|
type: page.type,
|
|
1130
1147
|
status: page.status,
|
|
1131
1148
|
spaceKey: page.space?.key,
|
|
1132
|
-
url:
|
|
1149
|
+
url: client.pageUrl(`/spaces/${page.space?.key}/pages/${page.id}`),
|
|
1133
1150
|
parentId: page.parentId || resolvedPageId
|
|
1134
1151
|
}))
|
|
1135
1152
|
};
|
|
@@ -1141,7 +1158,7 @@ program
|
|
|
1141
1158
|
|
|
1142
1159
|
// Build tree structure
|
|
1143
1160
|
const tree = buildTree(children, resolvedPageId);
|
|
1144
|
-
printTree(tree,
|
|
1161
|
+
printTree(tree, client, options, 1);
|
|
1145
1162
|
|
|
1146
1163
|
console.log('');
|
|
1147
1164
|
console.log(chalk.gray(`Total: ${children.length} child page${children.length === 1 ? '' : 's'}`));
|
|
@@ -1158,7 +1175,7 @@ program
|
|
|
1158
1175
|
}
|
|
1159
1176
|
|
|
1160
1177
|
if (options.showUrl) {
|
|
1161
|
-
const url =
|
|
1178
|
+
const url = client.pageUrl(`/spaces/${page.space?.key}/pages/${page.id}`);
|
|
1162
1179
|
output += `\n ${chalk.gray(url)}`;
|
|
1163
1180
|
}
|
|
1164
1181
|
|
|
@@ -1210,27 +1227,27 @@ function buildTree(pages, rootId) {
|
|
|
1210
1227
|
}
|
|
1211
1228
|
|
|
1212
1229
|
// Helper function to print tree
|
|
1213
|
-
function printTree(nodes,
|
|
1230
|
+
function printTree(nodes, client, options, depth = 1) {
|
|
1214
1231
|
nodes.forEach((node, index) => {
|
|
1215
1232
|
const isLast = index === nodes.length - 1;
|
|
1216
1233
|
const indent = ' '.repeat(depth - 1);
|
|
1217
1234
|
const prefix = isLast ? '└── ' : '├── ';
|
|
1218
|
-
|
|
1235
|
+
|
|
1219
1236
|
let output = `${indent}${prefix}📄 ${chalk.green(node.title)}`;
|
|
1220
|
-
|
|
1237
|
+
|
|
1221
1238
|
if (options.showId) {
|
|
1222
1239
|
output += ` ${chalk.gray(`(ID: ${node.id})`)}`;
|
|
1223
1240
|
}
|
|
1224
|
-
|
|
1241
|
+
|
|
1225
1242
|
if (options.showUrl) {
|
|
1226
|
-
const url =
|
|
1243
|
+
const url = client.pageUrl(`/spaces/${node.space?.key}/pages/${node.id}`);
|
|
1227
1244
|
output += `\n${indent}${isLast ? ' ' : '│ '}${chalk.gray(url)}`;
|
|
1228
1245
|
}
|
|
1229
|
-
|
|
1246
|
+
|
|
1230
1247
|
console.log(output);
|
|
1231
|
-
|
|
1248
|
+
|
|
1232
1249
|
if (node.children && node.children.length > 0) {
|
|
1233
|
-
printTree(node.children,
|
|
1250
|
+
printTree(node.children, client, options, depth + 1);
|
|
1234
1251
|
}
|
|
1235
1252
|
});
|
|
1236
1253
|
}
|
package/lib/confluence-client.js
CHANGED
|
@@ -23,6 +23,25 @@ class ConfluenceClient {
|
|
|
23
23
|
baseURL: this.baseURL,
|
|
24
24
|
headers
|
|
25
25
|
});
|
|
26
|
+
|
|
27
|
+
// Auto-detect context path (e.g., "/wiki" or "") from first API response
|
|
28
|
+
this._contextPath = null;
|
|
29
|
+
this.client.interceptors.response.use((response) => {
|
|
30
|
+
if (this._contextPath === null && response.data?._links?.context !== undefined) {
|
|
31
|
+
this._contextPath = response.data._links.context;
|
|
32
|
+
}
|
|
33
|
+
return response;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a full page URL from a webui path (e.g., /spaces/SPACE/pages/123)
|
|
39
|
+
* Context path is auto-detected from API responses.
|
|
40
|
+
*/
|
|
41
|
+
pageUrl(path) {
|
|
42
|
+
if (!path) return '';
|
|
43
|
+
const context = this._contextPath || '';
|
|
44
|
+
return `https://${this.domain}${context}${path}`;
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
sanitizeStorageContent(content, options = {}) {
|
|
@@ -373,7 +392,7 @@ class ConfluenceClient {
|
|
|
373
392
|
const webui = page._links?.webui || '';
|
|
374
393
|
return {
|
|
375
394
|
title: page.title,
|
|
376
|
-
url:
|
|
395
|
+
url: this.pageUrl(webui)
|
|
377
396
|
};
|
|
378
397
|
}
|
|
379
398
|
return null;
|
|
@@ -467,7 +486,7 @@ class ConfluenceClient {
|
|
|
467
486
|
// Format: - [Page Title](URL)
|
|
468
487
|
const childPagesList = childPages.map(page => {
|
|
469
488
|
const webui = page._links?.webui || '';
|
|
470
|
-
const url =
|
|
489
|
+
const url = this.pageUrl(webui);
|
|
471
490
|
if (url) {
|
|
472
491
|
return `- [${page.title}](${url})`;
|
|
473
492
|
} else {
|
|
@@ -853,27 +872,18 @@ class ConfluenceClient {
|
|
|
853
872
|
* Convert HTML to native Confluence storage format
|
|
854
873
|
*/
|
|
855
874
|
htmlToConfluenceStorage(html) {
|
|
875
|
+
// MarkdownIt already produces valid HTML tags (h1-h6, p, strong, em, ul, ol, table, etc.)
|
|
876
|
+
// This function only transforms elements that need Confluence-specific handling:
|
|
877
|
+
// - li content wrapped in p (Confluence requirement)
|
|
878
|
+
// - pre/code → ac:structured-macro code blocks with CDATA
|
|
879
|
+
// - blockquote → info/warning/note macros
|
|
880
|
+
// - th/td content wrapped in p
|
|
881
|
+
// - hr → self-closing hr /
|
|
856
882
|
let storage = html;
|
|
857
883
|
|
|
858
|
-
//
|
|
859
|
-
storage = storage.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, '<h$1>$2</h$1>');
|
|
860
|
-
|
|
861
|
-
// Convert paragraphs
|
|
862
|
-
storage = storage.replace(/<p>(.*?)<\/p>/g, '<p>$1</p>');
|
|
863
|
-
|
|
864
|
-
// Convert strong/bold text
|
|
865
|
-
storage = storage.replace(/<strong>(.*?)<\/strong>/g, '<strong>$1</strong>');
|
|
866
|
-
|
|
867
|
-
// Convert emphasis/italic text
|
|
868
|
-
storage = storage.replace(/<em>(.*?)<\/em>/g, '<em>$1</em>');
|
|
869
|
-
|
|
870
|
-
// Convert unordered lists
|
|
871
|
-
storage = storage.replace(/<ul>(.*?)<\/ul>/gs, '<ul>$1</ul>');
|
|
884
|
+
// Wrap li content in p tags (Confluence requirement)
|
|
872
885
|
storage = storage.replace(/<li>(.*?)<\/li>/g, '<li><p>$1</p></li>');
|
|
873
886
|
|
|
874
|
-
// Convert ordered lists
|
|
875
|
-
storage = storage.replace(/<ol>(.*?)<\/ol>/gs, '<ol>$1</ol>');
|
|
876
|
-
|
|
877
887
|
// Convert code blocks to Confluence code macro
|
|
878
888
|
storage = storage.replace(/<pre><code(?:\s+class="language-(\w+)")?>(.*?)<\/code><\/pre>/gs, (_, lang, code) => {
|
|
879
889
|
const language = lang || 'text';
|
|
@@ -892,9 +902,6 @@ class ConfluenceClient {
|
|
|
892
902
|
</ac:structured-macro>`;
|
|
893
903
|
});
|
|
894
904
|
|
|
895
|
-
// Convert inline code
|
|
896
|
-
storage = storage.replace(/<code>(.*?)<\/code>/g, '<code>$1</code>');
|
|
897
|
-
|
|
898
905
|
// Convert blockquotes to appropriate macros based on content
|
|
899
906
|
storage = storage.replace(/<blockquote>(.*?)<\/blockquote>/gs, (_, content) => {
|
|
900
907
|
// Check for admonition patterns
|
|
@@ -921,11 +928,7 @@ class ConfluenceClient {
|
|
|
921
928
|
}
|
|
922
929
|
});
|
|
923
930
|
|
|
924
|
-
//
|
|
925
|
-
storage = storage.replace(/<table>(.*?)<\/table>/gs, '<table>$1</table>');
|
|
926
|
-
storage = storage.replace(/<thead>(.*?)<\/thead>/gs, '<thead>$1</thead>');
|
|
927
|
-
storage = storage.replace(/<tbody>(.*?)<\/tbody>/gs, '<tbody>$1</tbody>');
|
|
928
|
-
storage = storage.replace(/<tr>(.*?)<\/tr>/gs, '<tr>$1</tr>');
|
|
931
|
+
// Wrap th/td content in p tags (Confluence requirement)
|
|
929
932
|
storage = storage.replace(/<th>(.*?)<\/th>/g, '<th><p>$1</p></th>');
|
|
930
933
|
storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
|
|
931
934
|
|
|
@@ -934,9 +937,6 @@ class ConfluenceClient {
|
|
|
934
937
|
// Convert horizontal rules
|
|
935
938
|
storage = storage.replace(/<hr\s*\/?>/g, '<hr />');
|
|
936
939
|
|
|
937
|
-
// Clean up any remaining HTML entities and normalize whitespace
|
|
938
|
-
storage = storage.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
939
|
-
|
|
940
940
|
return storage;
|
|
941
941
|
}
|
|
942
942
|
|
|
@@ -1154,12 +1154,12 @@ class ConfluenceClient {
|
|
|
1154
1154
|
markdown = markdown.replace(/<ac:structured-macro ac:name="include"[^>]*>[\s\S]*?<ac:parameter ac:name="">[\s\S]*?<ac:link>[\s\S]*?<ri:page\s+ri:space-key="([^"]+)"\s+ri:content-title="([^"]+)"[^>]*\/>[\s\S]*?<\/ac:link>[\s\S]*?<\/ac:parameter>[\s\S]*?<\/ac:structured-macro>/g, (_, spaceKey, title) => {
|
|
1155
1155
|
// Try to build a proper URL - if spaceKey starts with ~, it's a user space
|
|
1156
1156
|
if (spaceKey.startsWith('~')) {
|
|
1157
|
-
const spacePath =
|
|
1158
|
-
return `\n> 📄 **${labels.includePage}**: [${title}](
|
|
1157
|
+
const spacePath = `/display/${spaceKey}/${encodeURIComponent(title)}`;
|
|
1158
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](${this.pageUrl(spacePath)})\n`;
|
|
1159
1159
|
} else {
|
|
1160
1160
|
// For non-user spaces, we cannot construct a valid link without the page ID.
|
|
1161
1161
|
// Document that manual correction is required.
|
|
1162
|
-
return `\n> 📄 **${labels.includePage}**: [${title}](
|
|
1162
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](${this.pageUrl(`/spaces/${spaceKey}/pages/[PAGE_ID_HERE]`)}) _(manual link correction required)_\n`;
|
|
1163
1163
|
}
|
|
1164
1164
|
});
|
|
1165
1165
|
|