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.
- package/.eslintrc.cjs +18 -0
- package/.github/copilot-instructions.md +3 -0
- package/.github/prompts/analyze.prompt.md +101 -0
- package/.github/prompts/clarify.prompt.md +158 -0
- package/.github/prompts/constitution.prompt.md +73 -0
- package/.github/prompts/implement.prompt.md +56 -0
- package/.github/prompts/plan.prompt.md +50 -0
- package/.github/prompts/specify.prompt.md +21 -0
- package/.github/prompts/tasks.prompt.md +69 -0
- package/LICENSE +21 -0
- package/README.md +332 -0
- package/agents.md +1174 -0
- package/dist/api.d.ts +73 -0
- package/dist/api.js +387 -0
- package/dist/api.js.map +1 -0
- package/dist/commands/download.command.d.ts +18 -0
- package/dist/commands/download.command.js +257 -0
- package/dist/commands/download.command.js.map +1 -0
- package/dist/commands/executor.d.ts +22 -0
- package/dist/commands/executor.js +52 -0
- package/dist/commands/executor.js.map +1 -0
- package/dist/commands/help.command.d.ts +8 -0
- package/dist/commands/help.command.js +68 -0
- package/dist/commands/help.command.js.map +1 -0
- package/dist/commands/index.command.d.ts +14 -0
- package/dist/commands/index.command.js +95 -0
- package/dist/commands/index.command.js.map +1 -0
- package/dist/commands/index.d.ts +13 -0
- package/dist/commands/index.js +13 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/plan.command.d.ts +54 -0
- package/dist/commands/plan.command.js +272 -0
- package/dist/commands/plan.command.js.map +1 -0
- package/dist/commands/registry.d.ts +12 -0
- package/dist/commands/registry.js +32 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/transform.command.d.ts +69 -0
- package/dist/commands/transform.command.js +951 -0
- package/dist/commands/transform.command.js.map +1 -0
- package/dist/commands/types.d.ts +12 -0
- package/dist/commands/types.js +5 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/update.command.d.ts +10 -0
- package/dist/commands/update.command.js +201 -0
- package/dist/commands/update.command.js.map +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.js +52 -0
- package/dist/logger.js.map +1 -0
- package/dist/types.d.ts +167 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +56 -0
- package/dist/utils.js +178 -0
- package/dist/utils.js.map +1 -0
- package/eslint.config.js +29 -0
- package/jest.config.cjs +25 -0
- package/migrate-meta.js +132 -0
- package/package.json +53 -0
- package/src/api.ts +469 -0
- package/src/commands/download.command.ts +324 -0
- package/src/commands/executor.ts +62 -0
- package/src/commands/help.command.ts +72 -0
- package/src/commands/index.command.ts +111 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/plan.command.ts +318 -0
- package/src/commands/registry.ts +39 -0
- package/src/commands/transform.command.ts +1103 -0
- package/src/commands/types.ts +16 -0
- package/src/commands/update.command.ts +229 -0
- package/src/constants.ts +0 -0
- package/src/index.ts +120 -0
- package/src/logger.ts +60 -0
- package/src/test.sh +66 -0
- package/src/types.ts +176 -0
- package/src/utils.ts +204 -0
- package/tests/commands/README.md +123 -0
- package/tests/commands/download.command.test.ts +8 -0
- package/tests/commands/help.command.test.ts +8 -0
- package/tests/commands/index.command.test.ts +8 -0
- package/tests/commands/plan.command.test.ts +15 -0
- package/tests/commands/transform.command.test.ts +8 -0
- package/tests/fixtures/_index.yaml +38 -0
- package/tests/fixtures/mock-pages.ts +62 -0
- package/tsconfig.json +25 -0
- 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
|
+
}
|