confluence-exporter 1.0.0

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 (91) hide show
  1. package/.eslintrc.cjs +18 -0
  2. package/.github/copilot-instructions.md +3 -0
  3. package/.github/prompts/analyze.prompt.md +101 -0
  4. package/.github/prompts/clarify.prompt.md +158 -0
  5. package/.github/prompts/constitution.prompt.md +73 -0
  6. package/.github/prompts/implement.prompt.md +56 -0
  7. package/.github/prompts/plan.prompt.md +50 -0
  8. package/.github/prompts/specify.prompt.md +21 -0
  9. package/.github/prompts/tasks.prompt.md +69 -0
  10. package/LICENSE +21 -0
  11. package/README.md +332 -0
  12. package/agents.md +1174 -0
  13. package/dist/api.d.ts +73 -0
  14. package/dist/api.js +387 -0
  15. package/dist/api.js.map +1 -0
  16. package/dist/commands/download.command.d.ts +18 -0
  17. package/dist/commands/download.command.js +257 -0
  18. package/dist/commands/download.command.js.map +1 -0
  19. package/dist/commands/executor.d.ts +22 -0
  20. package/dist/commands/executor.js +52 -0
  21. package/dist/commands/executor.js.map +1 -0
  22. package/dist/commands/help.command.d.ts +8 -0
  23. package/dist/commands/help.command.js +68 -0
  24. package/dist/commands/help.command.js.map +1 -0
  25. package/dist/commands/index.command.d.ts +14 -0
  26. package/dist/commands/index.command.js +95 -0
  27. package/dist/commands/index.command.js.map +1 -0
  28. package/dist/commands/index.d.ts +13 -0
  29. package/dist/commands/index.js +13 -0
  30. package/dist/commands/index.js.map +1 -0
  31. package/dist/commands/plan.command.d.ts +54 -0
  32. package/dist/commands/plan.command.js +272 -0
  33. package/dist/commands/plan.command.js.map +1 -0
  34. package/dist/commands/registry.d.ts +12 -0
  35. package/dist/commands/registry.js +32 -0
  36. package/dist/commands/registry.js.map +1 -0
  37. package/dist/commands/transform.command.d.ts +69 -0
  38. package/dist/commands/transform.command.js +951 -0
  39. package/dist/commands/transform.command.js.map +1 -0
  40. package/dist/commands/types.d.ts +12 -0
  41. package/dist/commands/types.js +5 -0
  42. package/dist/commands/types.js.map +1 -0
  43. package/dist/commands/update.command.d.ts +10 -0
  44. package/dist/commands/update.command.js +201 -0
  45. package/dist/commands/update.command.js.map +1 -0
  46. package/dist/constants.d.ts +1 -0
  47. package/dist/constants.js +2 -0
  48. package/dist/constants.js.map +1 -0
  49. package/dist/index.d.ts +5 -0
  50. package/dist/index.js +110 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/logger.d.ts +15 -0
  53. package/dist/logger.js +52 -0
  54. package/dist/logger.js.map +1 -0
  55. package/dist/types.d.ts +167 -0
  56. package/dist/types.js +5 -0
  57. package/dist/types.js.map +1 -0
  58. package/dist/utils.d.ts +56 -0
  59. package/dist/utils.js +178 -0
  60. package/dist/utils.js.map +1 -0
  61. package/eslint.config.js +29 -0
  62. package/jest.config.cjs +25 -0
  63. package/migrate-meta.js +132 -0
  64. package/package.json +53 -0
  65. package/src/api.ts +469 -0
  66. package/src/commands/download.command.ts +324 -0
  67. package/src/commands/executor.ts +62 -0
  68. package/src/commands/help.command.ts +72 -0
  69. package/src/commands/index.command.ts +111 -0
  70. package/src/commands/index.ts +14 -0
  71. package/src/commands/plan.command.ts +318 -0
  72. package/src/commands/registry.ts +39 -0
  73. package/src/commands/transform.command.ts +1103 -0
  74. package/src/commands/types.ts +16 -0
  75. package/src/commands/update.command.ts +229 -0
  76. package/src/constants.ts +0 -0
  77. package/src/index.ts +120 -0
  78. package/src/logger.ts +60 -0
  79. package/src/test.sh +66 -0
  80. package/src/types.ts +176 -0
  81. package/src/utils.ts +204 -0
  82. package/tests/commands/README.md +123 -0
  83. package/tests/commands/download.command.test.ts +8 -0
  84. package/tests/commands/help.command.test.ts +8 -0
  85. package/tests/commands/index.command.test.ts +8 -0
  86. package/tests/commands/plan.command.test.ts +15 -0
  87. package/tests/commands/transform.command.test.ts +8 -0
  88. package/tests/fixtures/_index.yaml +38 -0
  89. package/tests/fixtures/mock-pages.ts +62 -0
  90. package/tsconfig.json +25 -0
  91. package/vite.config.ts +45 -0
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Plan command handler - Creates _queue.yaml for download
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import path from 'path';
7
+ import yaml from 'yaml';
8
+ import type { ConfluenceConfig, PageIndexEntry, PageTreeNode } from '../types.js';
9
+ import type { CommandContext, CommandHandler } from './types.js';
10
+ import { findExistingFile, checkPageStatus, updateIndexEntry } from '../utils.js';
11
+
12
+ export class PlanCommand implements CommandHandler {
13
+ queuePath: string;
14
+ treePath: string;
15
+ tree: PageTreeNode[];
16
+
17
+ constructor(private config: ConfluenceConfig) {
18
+ this.queuePath = path.join(this.config.outputDir, '_queue.yaml');
19
+ this.treePath = path.join(this.config.outputDir, '_tree.yaml');
20
+ this.tree = [];
21
+ }
22
+
23
+ async execute(context: CommandContext): Promise<void> {
24
+
25
+ // Create output directory if it doesn't exist
26
+ fs.mkdirSync(this.config.outputDir, { recursive: true });
27
+
28
+ // Check for --force flag
29
+ const forceMode = this.config.force === true;
30
+ if (forceMode) {
31
+ console.log('\n⚠️ Force mode enabled - all pages will be queued regardless of status\n');
32
+ }
33
+
34
+ // Step 1: Build complete tree structure (no limits applied here)
35
+ this.tree = this.buildTreeFromIndex();
36
+
37
+ if (this.config.pageId) {
38
+ // Build tree from specific page and all children
39
+ console.log(`Building tree from specific page: ${this.config.pageId}`);
40
+ this.tree = [this.collectPageTree(this.config.pageId)];
41
+ }
42
+
43
+ // Step 2: Create queue from tree with smart filtering
44
+ console.log(`\nCreating download queue from tree...`);
45
+ try {
46
+ const allPages = this.flattenTreeArray(this.tree);
47
+ console.log(`Flattened tree to ${allPages.length} pages`);
48
+
49
+ // Apply smart filtering (check if pages need downloading)
50
+ const { pagesToQueue, stats } = this.filterPagesForQueue(allPages, forceMode);
51
+
52
+ // Apply limit if specified (only affects queue, not tree)
53
+ const finalQueue = this.config.limit ? pagesToQueue.slice(0, this.config.limit) : pagesToQueue;
54
+
55
+ if (this.config.limit && pagesToQueue.length > this.config.limit) {
56
+ console.log(`Limiting queue to first ${this.config.limit} pages`);
57
+ }
58
+
59
+ this.writeQueue(this.queuePath, this.config, finalQueue);
60
+
61
+ console.log(`\n✓ Queue created: ${this.queuePath}`);
62
+ console.log(` 📊 Statistics:`);
63
+ console.log(` New pages: ${stats.new}`);
64
+ console.log(` Updated pages: ${stats.updated}`);
65
+ console.log(` Skipped: ${stats.skipped} (up-to-date)`);
66
+ console.log(` Total queued: ${finalQueue.length}`);
67
+ console.log(` Total in tree: ${allPages.length}`);
68
+ } catch (error) {
69
+ throw new Error(`Failed to create queue from tree: ${error instanceof Error ? error.message : error}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Recursively collect a page and all its descendants
75
+ */
76
+ private collectPageTree(pageId: string, depth: number = 0): PageTreeNode {
77
+ const indent = ' '.repeat(depth);
78
+
79
+ // Find the page in the tree
80
+ const node = this.findNodeInTree(this.tree, pageId);
81
+ if (!node) {
82
+ throw new Error(`Page ${pageId} not found in tree`);
83
+ }
84
+
85
+ console.log(`${indent}Found page: ${node.title} (${node.id})`);
86
+ return node;
87
+ }
88
+
89
+ /**
90
+ * Find a node by ID in the tree structure
91
+ */
92
+ private findNodeInTree(nodes: PageTreeNode[], pageId: string): PageTreeNode | null {
93
+ for (const node of nodes) {
94
+ if (node.id === pageId) {
95
+ return node;
96
+ }
97
+ if (node.children && node.children.length > 0) {
98
+ const found = this.findNodeInTree(node.children, pageId);
99
+ if (found) {
100
+ return found;
101
+ }
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Flatten tree structure to array of PageIndexEntry
109
+ */
110
+ private flattenTree(node: PageTreeNode, result: PageIndexEntry[] = []): PageIndexEntry[] {
111
+ result.push({
112
+ id: node.id,
113
+ title: node.title,
114
+ version: node.version,
115
+ parentId: node.parentId,
116
+ modifiedDate: node.modifiedDate,
117
+ indexedDate: new Date().toISOString(),
118
+ pageNumber: 0
119
+ });
120
+
121
+ if (node.children) {
122
+ for (const child of node.children) {
123
+ this.flattenTree(child, result);
124
+ }
125
+ }
126
+
127
+ return result;
128
+ }
129
+
130
+ /**
131
+ * Flatten array of tree nodes to array of PageIndexEntry
132
+ */
133
+ private flattenTreeArray(trees: PageTreeNode[]): PageIndexEntry[] {
134
+ const result: PageIndexEntry[] = [];
135
+ for (const tree of trees) {
136
+ this.flattenTree(tree, result);
137
+ }
138
+ return result;
139
+ }
140
+
141
+ /**
142
+ * Filter pages based on whether they need to be downloaded
143
+ * Checks for existing files and compares versions/dates
144
+ *
145
+ * @param pages - All pages from the tree
146
+ * @param forceMode - If true, skip all checks and include all pages
147
+ * @returns Filtered pages with queueReason and statistics
148
+ */
149
+ private filterPagesForQueue(
150
+ pages: PageIndexEntry[],
151
+ forceMode: boolean
152
+ ): { pagesToQueue: PageIndexEntry[]; stats: { new: number; updated: number; skipped: number } } {
153
+ const pagesToQueue: PageIndexEntry[] = [];
154
+ const stats = { new: 0, updated: 0, skipped: 0 };
155
+
156
+ console.log('\n📋 Checking page status...');
157
+
158
+ // Force mode: include all pages without checking
159
+ if (forceMode) {
160
+ for (const page of pages) {
161
+ pagesToQueue.push({ ...page, queueReason: 'new' });
162
+ stats.new++;
163
+ }
164
+ return { pagesToQueue, stats };
165
+ }
166
+
167
+ // Read index once for all lookups
168
+ const indexPath = path.join(this.config.outputDir, '_index.yaml');
169
+ const indexEntries = this.loadIndexEntries(indexPath);
170
+ const indexMap = new Map(indexEntries.map(entry => [entry.id, entry]));
171
+
172
+ console.log(`Loaded ${indexEntries.length} entries from index for status checking`);
173
+
174
+ // Process pages in batches of 100 for better performance
175
+ const batchSize = 100;
176
+ for (let i = 0; i < pages.length; i += batchSize) {
177
+ const batch = pages.slice(i, i + batchSize);
178
+ console.log(`Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(pages.length / batchSize)} (${batch.length} pages)`);
179
+
180
+ for (const page of batch) {
181
+ // Check if page has been downloaded by looking at index entry
182
+ const existingEntry = indexMap.get(page.id);
183
+
184
+ if (!existingEntry || !existingEntry.downloadedVersion) {
185
+ // Page not downloaded yet
186
+ console.log(` [NEW] ${page.title} (${page.id})`);
187
+ pagesToQueue.push({ ...page, queueReason: 'new' });
188
+ stats.new++;
189
+ continue;
190
+ }
191
+
192
+ // Check if page needs update
193
+ const status = checkPageStatus(existingEntry);
194
+
195
+ if (status.needsDownload) {
196
+ const details = status.details ? ` - ${status.details}` : '';
197
+ console.log(` [UPDATE] ${page.title} (${page.id})${details}`);
198
+ pagesToQueue.push({ ...page, queueReason: 'updated' });
199
+ stats.updated++;
200
+ } else {
201
+ console.log(` [SKIP] ${page.title} (${page.id}) - up to date (v${existingEntry.downloadedVersion})`);
202
+ stats.skipped++;
203
+ }
204
+ }
205
+ }
206
+
207
+ return { pagesToQueue, stats };
208
+ }
209
+
210
+ /**
211
+ * Load all index entries from _index.yaml file
212
+ */
213
+ private loadIndexEntries(indexPath: string): PageIndexEntry[] {
214
+ if (!fs.existsSync(indexPath)) return [];
215
+
216
+ try {
217
+ const content = fs.readFileSync(indexPath, 'utf-8');
218
+ return yaml.parse(content) as PageIndexEntry[];
219
+ } catch (error) {
220
+ console.warn(`Warning: Failed to load index file ${indexPath}: ${error}`);
221
+ return [];
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Build tree structure from flat index
227
+ */
228
+ private buildTreeFromIndex(): PageTreeNode[] {
229
+ const indexPath = path.join(this.config.outputDir, '_index.yaml');
230
+
231
+ const yamlContent = fs.readFileSync(indexPath, 'utf-8');
232
+ const pages = yaml.parse(yamlContent) as PageIndexEntry[];
233
+
234
+ console.log(`Read ${pages.length} pages from _index.yaml`);
235
+
236
+ const nodeMap = new Map<string, PageTreeNode>();
237
+ const roots: PageTreeNode[] = [];
238
+
239
+ // First pass: create all nodes
240
+ for (const page of pages) {
241
+ nodeMap.set(page.id, {
242
+ id: page.id,
243
+ title: page.title,
244
+ version: page.version,
245
+ parentId: page.parentId,
246
+ modifiedDate: page.modifiedDate,
247
+ children: []
248
+ });
249
+ }
250
+
251
+ // Second pass: build tree structure
252
+ for (const page of pages) {
253
+ const node = nodeMap.get(page.id);
254
+ if (!node) continue;
255
+
256
+ if (page.parentId && nodeMap.has(page.parentId)) {
257
+ // Add as child to parent
258
+ const parent = nodeMap.get(page.parentId);
259
+ if (parent?.children) {
260
+ parent.children.push(node);
261
+ }
262
+ } else {
263
+ // No parent or parent not in index - it's a root
264
+ roots.push(node);
265
+ }
266
+ }
267
+ this.writeTree(roots);
268
+ console.log(`✓ Complete tree structure saved: ${this.treePath}`);
269
+
270
+ return roots;
271
+ }
272
+
273
+ /**
274
+ * Write _tree.yaml file with hierarchical structure
275
+ */
276
+ private writeTree(tree: PageTreeNode[]): void {
277
+ const header = `# Confluence Page Tree
278
+ # Space: ${this.config.spaceKey}
279
+ # Created: ${new Date().toISOString()}
280
+
281
+ `;
282
+
283
+ const yamlContent = yaml.stringify(tree, {
284
+ indent: 2,
285
+ lineWidth: 0 // No line wrapping
286
+ });
287
+
288
+ fs.writeFileSync(this.treePath, header + yamlContent, 'utf-8');
289
+ }
290
+
291
+ /**
292
+ * Write _queue.yaml file
293
+ */
294
+ private writeQueue(queuePath: string, config: CommandContext['config'], pages: PageIndexEntry[]): void {
295
+ const header = `# Confluence Download Queue
296
+ # Space: ${config.spaceKey}
297
+ # Created: ${new Date().toISOString()}
298
+ # Total Pages: ${pages.length}
299
+
300
+ `;
301
+
302
+ fs.writeFileSync(queuePath, header, 'utf-8');
303
+
304
+ // Write each page as YAML array entry
305
+ for (const page of pages) {
306
+ const yamlDoc = yaml.stringify(page).trim();
307
+ const lines = yamlDoc.split('\n');
308
+ const arrayItem = lines.map((line, index) => {
309
+ if (index === 0) {
310
+ return `- ${line}`;
311
+ }
312
+ return ` ${line}`;
313
+ }).join('\n');
314
+
315
+ fs.appendFileSync(queuePath, arrayItem + '\n', 'utf-8');
316
+ }
317
+ }
318
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Command registry - maps command names to handlers
3
+ */
4
+
5
+ import { HelpCommand } from './help.command.js';
6
+ import { IndexCommand } from './index.command.js';
7
+ import { UpdateCommand } from './update.command.js';
8
+ import { PlanCommand } from './plan.command.js';
9
+ import { DownloadCommand } from './download.command.js';
10
+ import { TransformCommand } from './transform.command.js';
11
+ import type { Command, CommandContext, CommandHandler } from './types.js';
12
+ import { ConfluenceConfig } from 'src/types.js';
13
+
14
+ export class CommandRegistry {
15
+ private handlers: Map<Command, CommandHandler>;
16
+
17
+ constructor(config?: ConfluenceConfig) {
18
+ this.handlers = new Map<Command, CommandHandler>([
19
+ ['help', new HelpCommand()],
20
+ ['index', new IndexCommand(config)],
21
+ ['update', new UpdateCommand(config)],
22
+ ['plan', new PlanCommand(config)],
23
+ ['download', new DownloadCommand(config)],
24
+ ['transform', new TransformCommand(config)]
25
+ ]);
26
+ }
27
+
28
+ getHandler(command: Command): CommandHandler | undefined {
29
+ return this.handlers.get(command);
30
+ }
31
+
32
+ isValidCommand(command: string): command is Command {
33
+ return this.handlers.has(command as Command);
34
+ }
35
+
36
+ getValidCommands(): Command[] {
37
+ return Array.from(this.handlers.keys());
38
+ }
39
+ }