create-confluence-sync 1.0.3 → 1.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-confluence-sync",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Bidirectional Confluence Server documentation sync via Git. Edit XHTML locally, commit, auto-sync with Confluence.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -184,6 +184,32 @@ export function createApiClient(config) {
184
184
  };
185
185
  }
186
186
 
187
+ async function movePage(pageId, newParentId, title, body, currentVersion) {
188
+ const payload = {
189
+ type: 'page',
190
+ title,
191
+ version: { number: currentVersion + 1 },
192
+ body: {
193
+ storage: {
194
+ value: body,
195
+ representation: 'storage',
196
+ },
197
+ },
198
+ ancestors: [{ id: String(newParentId) }],
199
+ };
200
+
201
+ const data = await jsonRequest(
202
+ baseUrl, token, 'PUT',
203
+ `/rest/api/content/${pageId}`,
204
+ payload
205
+ );
206
+
207
+ return {
208
+ id: String(data.id),
209
+ version: data.version.number,
210
+ };
211
+ }
212
+
187
213
  async function downloadAttachment(downloadPath) {
188
214
  const { buffer } = await request(baseUrl, token, 'GET', downloadPath);
189
215
  return buffer;
@@ -201,6 +227,7 @@ export function createApiClient(config) {
201
227
  getPageAttachments,
202
228
  createPage,
203
229
  updatePage,
230
+ movePage,
204
231
  deletePage,
205
232
  downloadAttachment,
206
233
  };
package/src/git.js CHANGED
@@ -23,6 +23,24 @@ function getDeletedFiles(projectRoot) {
23
23
  return output.split('\n').filter(f => f.endsWith('.xhtml'));
24
24
  }
25
25
 
26
+ function getRenamedFiles(projectRoot) {
27
+ let output;
28
+ try {
29
+ output = exec(projectRoot, 'git -c core.quotePath=false diff --name-status -M HEAD~1 HEAD');
30
+ } catch {
31
+ return [];
32
+ }
33
+ if (!output) return [];
34
+ const renames = [];
35
+ for (const line of output.split('\n')) {
36
+ const match = line.match(/^R\d+\t(.+)\t(.+)$/);
37
+ if (match && match[2].endsWith('.xhtml')) {
38
+ renames.push({ from: match[1], to: match[2] });
39
+ }
40
+ }
41
+ return renames;
42
+ }
43
+
26
44
  function getChangedFilesUncommitted(projectRoot) {
27
45
  const output = exec(projectRoot, 'git diff --name-only HEAD');
28
46
  if (!output) return [];
@@ -123,6 +141,7 @@ function initRepo(projectRoot) {
123
141
  export {
124
142
  getChangedFiles,
125
143
  getDeletedFiles,
144
+ getRenamedFiles,
126
145
  getChangedFilesUncommitted,
127
146
  createBranch,
128
147
  switchBranch,
package/src/hook.js CHANGED
@@ -6,6 +6,7 @@ import { pull, push, detectHidden } from './sync.js';
6
6
  import {
7
7
  getChangedFiles,
8
8
  getDeletedFiles,
9
+ getRenamedFiles,
9
10
  getCurrentBranch,
10
11
  createBranch,
11
12
  switchBranch,
@@ -60,9 +61,13 @@ async function main() {
60
61
  // Create API client
61
62
  const apiClient = createApiClient(config);
62
63
 
63
- // Detect hidden (only check files deleted in this commit)
64
+ // Detect renames (must happen before detectHidden to exclude renamed files)
65
+ const renames = getRenamedFiles(projectRoot);
66
+ const renamedFromPaths = new Set(renames.map(r => r.from.replace(/\\/g, '/')));
67
+
68
+ // Detect hidden (only check files deleted in this commit, excluding renames)
64
69
  const deletedFiles = getDeletedFiles(projectRoot);
65
- const hiddenCount = detectHidden(tree, projectRoot, deletedFiles);
70
+ const hiddenCount = detectHidden(tree, projectRoot, deletedFiles, renamedFromPaths);
66
71
  if (hiddenCount > 0) {
67
72
  log(`Hidden: ${hiddenCount} pages marked as hidden`);
68
73
  }
@@ -102,11 +107,11 @@ async function main() {
102
107
  deleteBranch(projectRoot, 'confluence/pull');
103
108
 
104
109
  // --- Push (Local -> Confluence) ---
105
- const pushResult = await push(apiClient, tree, projectRoot, config.space, changedFiles);
110
+ const pushResult = await push(apiClient, tree, projectRoot, config.space, changedFiles, renames);
106
111
  saveTree(projectRoot, tree);
107
112
  commitAll(projectRoot, `${PREFIX} push to Confluence + update tree`);
108
113
 
109
- log(`Push: ${pushResult.created} created, ${pushResult.updated} updated`);
114
+ log(`Push: ${pushResult.created} created, ${pushResult.updated} updated, ${pushResult.moved} moved/renamed`);
110
115
  log('Sync complete!');
111
116
  }
112
117
 
package/src/sync.js CHANGED
@@ -161,11 +161,61 @@ export async function pull(apiClient, tree, projectRoot, spaceKey) {
161
161
  return { created: created.length, updated: updated.length, deleted: deleted.length, renamed };
162
162
  }
163
163
 
164
- export async function push(apiClient, tree, projectRoot, spaceKey, changedFiles) {
164
+ export async function push(apiClient, tree, projectRoot, spaceKey, changedFiles, renames = []) {
165
165
  let created = 0;
166
166
  let updated = 0;
167
+ let moved = 0;
167
168
 
168
- for (let filePath of changedFiles) {
169
+ // --- Handle renames/moves before the main loop ---
170
+ const renamedFromPaths = new Set(renames.map(r => r.from.replace(/\\/g, '/')));
171
+ const renamedToPaths = new Set(renames.map(r => r.to.replace(/\\/g, '/')));
172
+
173
+ for (const { from, to } of renames) {
174
+ const fromNorm = from.replace(/\\/g, '/');
175
+ const toNorm = to.replace(/\\/g, '/');
176
+ const existing = getPageByPath(tree, fromNorm);
177
+
178
+ if (!existing) continue;
179
+
180
+ const pageId = existing.pageId;
181
+ const newTitle = path.basename(toNorm).replace(/\.xhtml$/, '');
182
+ const newParentId = pathToParentId(tree, toNorm);
183
+
184
+ const absolutePath = path.resolve(projectRoot, toNorm);
185
+ const body = await fs.readFile(absolutePath, 'utf-8');
186
+
187
+ let result;
188
+ const parentChanged = newParentId && newParentId !== existing.parentId;
189
+
190
+ if (parentChanged) {
191
+ result = await apiClient.movePage(pageId, newParentId, newTitle, body, existing.version);
192
+ console.log(`Moved: ${fromNorm} → ${toNorm}`);
193
+ } else {
194
+ result = await apiClient.updatePage(pageId, newTitle, body, existing.version);
195
+ console.log(`Renamed: ${fromNorm} → ${toNorm}`);
196
+ }
197
+
198
+ addPage(tree, pageId, {
199
+ title: newTitle,
200
+ fsName: sanitizeName(newTitle),
201
+ path: toNorm,
202
+ parentId: parentChanged ? newParentId : existing.parentId,
203
+ version: result.version,
204
+ });
205
+ moved++;
206
+ }
207
+
208
+ // Sort by path depth — create parents before children
209
+ const sorted = [...changedFiles].sort((a, b) => {
210
+ const depthA = a.replace(/\\/g, '/').split('/').length;
211
+ const depthB = b.replace(/\\/g, '/').split('/').length;
212
+ return depthA - depthB;
213
+ });
214
+
215
+ for (let filePath of sorted) {
216
+ // Skip files that are part of a rename operation
217
+ const normalizedCheck = filePath.replace(/\\/g, '/');
218
+ if (renamedToPaths.has(normalizedCheck)) continue;
169
219
  let normalized = filePath.replace(/\\/g, '/');
170
220
  let absolutePath = path.resolve(projectRoot, normalized);
171
221
 
@@ -220,16 +270,17 @@ export async function push(apiClient, tree, projectRoot, spaceKey, changedFiles)
220
270
 
221
271
  tree.lastSync = new Date().toISOString();
222
272
 
223
- return { created, updated };
273
+ return { created, updated, moved };
224
274
  }
225
275
 
226
- export function detectHidden(tree, projectRoot, deletedFiles) {
276
+ export function detectHidden(tree, projectRoot, deletedFiles, excludePaths = new Set()) {
227
277
  let count = 0;
228
278
 
229
279
  if (deletedFiles && deletedFiles.length > 0) {
230
280
  // Fast path: only check files we know were deleted in the commit
231
281
  for (const filePath of deletedFiles) {
232
282
  const normalized = filePath.replace(/\\/g, '/');
283
+ if (excludePaths.has(normalized)) continue;
233
284
  const page = getPageByPath(tree, normalized);
234
285
  if (page && !page.hidden) {
235
286
  console.log(`Hidden: ${normalized}`);