create-confluence-sync 1.0.4 → 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.4",
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,9 +161,49 @@ 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;
168
+
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
+ }
167
207
 
168
208
  // Sort by path depth — create parents before children
169
209
  const sorted = [...changedFiles].sort((a, b) => {
@@ -173,6 +213,9 @@ export async function push(apiClient, tree, projectRoot, spaceKey, changedFiles)
173
213
  });
174
214
 
175
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;
176
219
  let normalized = filePath.replace(/\\/g, '/');
177
220
  let absolutePath = path.resolve(projectRoot, normalized);
178
221
 
@@ -227,16 +270,17 @@ export async function push(apiClient, tree, projectRoot, spaceKey, changedFiles)
227
270
 
228
271
  tree.lastSync = new Date().toISOString();
229
272
 
230
- return { created, updated };
273
+ return { created, updated, moved };
231
274
  }
232
275
 
233
- export function detectHidden(tree, projectRoot, deletedFiles) {
276
+ export function detectHidden(tree, projectRoot, deletedFiles, excludePaths = new Set()) {
234
277
  let count = 0;
235
278
 
236
279
  if (deletedFiles && deletedFiles.length > 0) {
237
280
  // Fast path: only check files we know were deleted in the commit
238
281
  for (const filePath of deletedFiles) {
239
282
  const normalized = filePath.replace(/\\/g, '/');
283
+ if (excludePaths.has(normalized)) continue;
240
284
  const page = getPageByPath(tree, normalized);
241
285
  if (page && !page.hidden) {
242
286
  console.log(`Hidden: ${normalized}`);