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,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-related type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ConfluenceConfig } from '../types.js';
|
|
6
|
+
|
|
7
|
+
export type Command = 'help' | 'index' | 'update' | 'plan' | 'download' | 'transform';
|
|
8
|
+
|
|
9
|
+
export interface CommandContext {
|
|
10
|
+
config: ConfluenceConfig;
|
|
11
|
+
args: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CommandHandler {
|
|
15
|
+
execute(context: CommandContext): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update command handler - Checks for new/updated pages and updates _index.yaml
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import yaml from 'yaml';
|
|
8
|
+
import { ConfluenceApi } from '../api.js';
|
|
9
|
+
import type { ConfluenceConfig, PageIndexEntry, PageMetadata } from '../types.js';
|
|
10
|
+
import type { CommandContext, CommandHandler } from './types.js';
|
|
11
|
+
|
|
12
|
+
export class UpdateCommand implements CommandHandler {
|
|
13
|
+
constructor(private config: ConfluenceConfig) {}
|
|
14
|
+
|
|
15
|
+
async execute(context: CommandContext): Promise<void> {
|
|
16
|
+
const api = new ConfluenceApi(this.config);
|
|
17
|
+
const indexPath = path.join(this.config.outputDir, '_index.yaml');
|
|
18
|
+
|
|
19
|
+
console.log(`Checking for updates in space: ${this.config.spaceKey}`);
|
|
20
|
+
console.log(`Index file: ${indexPath}\n`);
|
|
21
|
+
|
|
22
|
+
// Load existing index
|
|
23
|
+
let existingIndex: Map<string, PageIndexEntry>;
|
|
24
|
+
let oldestIndexedDate: string | undefined;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const existingContent = await fs.readFile(indexPath, 'utf-8');
|
|
28
|
+
const existingPages = yaml.parse(existingContent) as PageIndexEntry[];
|
|
29
|
+
existingIndex = new Map(existingPages.map(p => [p.id, p]));
|
|
30
|
+
|
|
31
|
+
// Find the oldest indexedDate to use as the starting point for CQL search
|
|
32
|
+
for (const page of existingPages) {
|
|
33
|
+
if (page.indexedDate) {
|
|
34
|
+
if (!oldestIndexedDate || page.indexedDate < oldestIndexedDate) {
|
|
35
|
+
oldestIndexedDate = page.indexedDate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`Loaded existing index with ${existingIndex.size} pages`);
|
|
41
|
+
if (oldestIndexedDate) {
|
|
42
|
+
console.log(`Oldest indexed date: ${oldestIndexedDate}\n`);
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(`Error: _index.yaml not found. Run 'index' command first.`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Use CQL to fetch only pages modified since oldest indexed date
|
|
50
|
+
// This is much more efficient than fetching all pages
|
|
51
|
+
let modifiedPages: PageMetadata[] = [];
|
|
52
|
+
|
|
53
|
+
if (oldestIndexedDate) {
|
|
54
|
+
// Format date for CQL: "yyyy-MM-dd" or "yyyy-MM-dd HH:mm"
|
|
55
|
+
const searchDate = oldestIndexedDate.split('T')[0]; // Get just the date part
|
|
56
|
+
const cql = `space = "${this.config.spaceKey}" AND type = page AND lastmodified >= "${searchDate}" ORDER BY lastmodified DESC`;
|
|
57
|
+
|
|
58
|
+
console.log(`Searching for pages modified since ${searchDate}...`);
|
|
59
|
+
console.log(`CQL: ${cql}\n`);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
modifiedPages = await api.searchPages(cql, this.config.pageSize || 100);
|
|
63
|
+
console.log(`Found ${modifiedPages.length} pages modified since ${searchDate}\n`);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn(`CQL search failed, falling back to full space scan...`);
|
|
66
|
+
console.warn(`Error: ${error}\n`);
|
|
67
|
+
modifiedPages = [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// If CQL search returned results, use them; otherwise fall back to full scan
|
|
72
|
+
let currentPages: Map<string, PageMetadata>;
|
|
73
|
+
let needsFullScan = modifiedPages.length === 0;
|
|
74
|
+
|
|
75
|
+
if (!needsFullScan) {
|
|
76
|
+
// Build a map from modified pages and merge with existing index
|
|
77
|
+
// This approach won't detect deleted pages without a full scan
|
|
78
|
+
currentPages = new Map(Array.from(existingIndex.entries()).map(([id, entry]) => [id, {
|
|
79
|
+
id: entry.id,
|
|
80
|
+
title: entry.title,
|
|
81
|
+
version: entry.version,
|
|
82
|
+
parentId: entry.parentId,
|
|
83
|
+
modifiedDate: entry.modifiedDate
|
|
84
|
+
}] as [string, PageMetadata]));
|
|
85
|
+
|
|
86
|
+
// Update with modified pages
|
|
87
|
+
for (const page of modifiedPages) {
|
|
88
|
+
currentPages.set(page.id, page);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`Note: Using incremental update. To detect deleted pages, run with --full flag.\n`);
|
|
92
|
+
} else {
|
|
93
|
+
// Full scan - fetch all pages metadata
|
|
94
|
+
console.log('Fetching all pages metadata (metadata only, no content)...\n');
|
|
95
|
+
|
|
96
|
+
currentPages = new Map();
|
|
97
|
+
let pageCount = 0;
|
|
98
|
+
|
|
99
|
+
for await (const page of api.getAllPagesMetadata(this.config.spaceKey, this.config.pageSize || 100)) {
|
|
100
|
+
currentPages.set(page.id, page);
|
|
101
|
+
pageCount++;
|
|
102
|
+
|
|
103
|
+
// Progress indicator every 100 pages
|
|
104
|
+
if (pageCount % 100 === 0) {
|
|
105
|
+
console.log(` Fetched ${pageCount} pages...`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(`\nFetched ${currentPages.size} pages from space\n`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Compare and find changes
|
|
113
|
+
const newPages: PageMetadata[] = [];
|
|
114
|
+
const updatedPages: Array<{ current: PageMetadata; indexed: PageIndexEntry }> = [];
|
|
115
|
+
const deletedPages: PageIndexEntry[] = [];
|
|
116
|
+
|
|
117
|
+
// Find new and updated pages
|
|
118
|
+
for (const [id, current] of currentPages) {
|
|
119
|
+
const indexed = existingIndex.get(id);
|
|
120
|
+
|
|
121
|
+
if (!indexed) {
|
|
122
|
+
newPages.push(current);
|
|
123
|
+
} else if (current.version && indexed.version && current.version > indexed.version) {
|
|
124
|
+
updatedPages.push({ current, indexed });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Find deleted pages (only if we did a full scan)
|
|
129
|
+
if (!needsFullScan) {
|
|
130
|
+
// Skip deleted page detection for incremental updates
|
|
131
|
+
} else {
|
|
132
|
+
for (const [id, indexed] of existingIndex) {
|
|
133
|
+
if (!currentPages.has(id)) {
|
|
134
|
+
deletedPages.push(indexed);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Report findings
|
|
140
|
+
console.log('=== Update Summary ===');
|
|
141
|
+
console.log(`New pages: ${newPages.length}`);
|
|
142
|
+
console.log(`Updated pages: ${updatedPages.length}`);
|
|
143
|
+
if (needsFullScan) {
|
|
144
|
+
console.log(`Deleted pages: ${deletedPages.length}`);
|
|
145
|
+
} else {
|
|
146
|
+
console.log(`Deleted pages: (skipped - use --full for deletion detection)`);
|
|
147
|
+
}
|
|
148
|
+
console.log('');
|
|
149
|
+
|
|
150
|
+
if (newPages.length === 0 && updatedPages.length === 0 && deletedPages.length === 0) {
|
|
151
|
+
console.log('✓ Index is up to date. No changes needed.');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Show details
|
|
156
|
+
if (newPages.length > 0) {
|
|
157
|
+
console.log('\n--- New Pages ---');
|
|
158
|
+
for (const page of newPages) {
|
|
159
|
+
console.log(` + ${page.title} (${page.id})`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (updatedPages.length > 0) {
|
|
164
|
+
console.log('\n--- Updated Pages ---');
|
|
165
|
+
for (const { current, indexed } of updatedPages) {
|
|
166
|
+
console.log(` ~ ${current.title} (${current.id}) v${indexed.version} → v${current.version}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (deletedPages.length > 0) {
|
|
171
|
+
console.log('\n--- Deleted Pages ---');
|
|
172
|
+
for (const page of deletedPages) {
|
|
173
|
+
console.log(` - ${page.title} (${page.id})`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update the index
|
|
178
|
+
console.log('\n\nUpdating _index.yaml...');
|
|
179
|
+
|
|
180
|
+
// Build updated index
|
|
181
|
+
const updatedIndex: PageIndexEntry[] = [];
|
|
182
|
+
const now = new Date().toISOString();
|
|
183
|
+
|
|
184
|
+
for (const [id, current] of currentPages) {
|
|
185
|
+
const existing = existingIndex.get(id);
|
|
186
|
+
|
|
187
|
+
if (existing && (!current.version || !existing.version || current.version === existing.version)) {
|
|
188
|
+
// Keep existing entry unchanged
|
|
189
|
+
updatedIndex.push(existing);
|
|
190
|
+
} else {
|
|
191
|
+
// New or updated page
|
|
192
|
+
updatedIndex.push({
|
|
193
|
+
id: current.id,
|
|
194
|
+
title: current.title,
|
|
195
|
+
version: current.version,
|
|
196
|
+
parentId: current.parentId,
|
|
197
|
+
modifiedDate: current.modifiedDate,
|
|
198
|
+
indexedDate: now,
|
|
199
|
+
pageNumber: existing?.pageNumber || 0
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Write updated index
|
|
205
|
+
const header = `# Confluence Export Index
|
|
206
|
+
# Space: ${this.config.spaceKey}
|
|
207
|
+
# Export Date: ${now}
|
|
208
|
+
# Page Size: ${this.config.pageSize || 100}
|
|
209
|
+
|
|
210
|
+
`;
|
|
211
|
+
|
|
212
|
+
const yamlContent = updatedIndex.map(entry => {
|
|
213
|
+
const yamlDoc = yaml.stringify(entry).trim();
|
|
214
|
+
const lines = yamlDoc.split('\n');
|
|
215
|
+
return lines.map((line, index) => {
|
|
216
|
+
if (index === 0) {
|
|
217
|
+
return `- ${line}`;
|
|
218
|
+
}
|
|
219
|
+
return ` ${line}`;
|
|
220
|
+
}).join('\n');
|
|
221
|
+
}).join('\n');
|
|
222
|
+
|
|
223
|
+
await fs.writeFile(indexPath, header + yamlContent + '\n', 'utf-8');
|
|
224
|
+
|
|
225
|
+
console.log(`\n✓ Index updated successfully!`);
|
|
226
|
+
console.log(` Total pages in index: ${updatedIndex.length}`);
|
|
227
|
+
console.log(` Added: ${newPages.length}, Updated: ${updatedPages.length}, Removed: ${deletedPages.length}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
package/src/constants.ts
ADDED
|
File without changes
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Minimal Confluence to Markdown Exporter - CLI Entry Point
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import minimist from 'minimist';
|
|
7
|
+
import { config as loadEnv } from 'dotenv';
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { CommandExecutor } from './commands/executor.js';
|
|
11
|
+
import { HelpCommand } from './commands/help.command.js';
|
|
12
|
+
import type { ConfluenceConfig } from './types.js';
|
|
13
|
+
import type { CommandContext } from './commands/types.js';
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
// Load .env file if it exists
|
|
17
|
+
loadEnv();
|
|
18
|
+
|
|
19
|
+
// Parse command line arguments
|
|
20
|
+
const args = minimist(process.argv.slice(2), {
|
|
21
|
+
string: ['url', 'username', 'password', 'space', 'output', 'pageId', 'pageSize', 'limit', 'parallel'],
|
|
22
|
+
boolean: ['clear', 'force', 'debug'],
|
|
23
|
+
alias: {
|
|
24
|
+
u: 'url',
|
|
25
|
+
n: 'username',
|
|
26
|
+
p: 'password',
|
|
27
|
+
s: 'space',
|
|
28
|
+
o: 'output',
|
|
29
|
+
i: 'pageId',
|
|
30
|
+
l: 'limit',
|
|
31
|
+
f: 'force',
|
|
32
|
+
d: 'debug',
|
|
33
|
+
h: 'help'
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Show help if requested
|
|
38
|
+
if (args.help) {
|
|
39
|
+
const helpCommand = new HelpCommand();
|
|
40
|
+
await helpCommand.execute({ config: {} as ConfluenceConfig, args });
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Build config from args or environment variables
|
|
45
|
+
const config: ConfluenceConfig = {
|
|
46
|
+
baseUrl: args.url || process.env.CONFLUENCE_BASE_URL || '',
|
|
47
|
+
username: args.username || process.env.CONFLUENCE_USERNAME || '',
|
|
48
|
+
password: args.password || process.env.CONFLUENCE_PASSWORD || '',
|
|
49
|
+
spaceKey: args.space || process.env.CONFLUENCE_SPACE_KEY || '',
|
|
50
|
+
outputDir: args.output || process.env.OUTPUT_DIR || './output',
|
|
51
|
+
pageId: args.pageId || undefined,
|
|
52
|
+
pageSize: args.pageSize ? parseInt(args.pageSize, 10) : 100,
|
|
53
|
+
limit: args.limit ? parseInt(args.limit, 10) : undefined,
|
|
54
|
+
clear: args.clear || false,
|
|
55
|
+
force: args.force || false,
|
|
56
|
+
debug: args.debug || false,
|
|
57
|
+
parallel: args.parallel ? parseInt(args.parallel, 10) : 5
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Extract commands from positional arguments
|
|
61
|
+
let commands: string[];
|
|
62
|
+
if (args._.length === 0) {
|
|
63
|
+
// No commands provided - run full sync workflow
|
|
64
|
+
const indexPath = path.join(config.outputDir, '_index.yaml');
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(indexPath);
|
|
67
|
+
commands = ['update', 'plan', 'download', 'transform'];
|
|
68
|
+
} catch {
|
|
69
|
+
commands = ['index', 'plan', 'download', 'transform'];
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
commands = args._ as string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Configure logger debug mode if requested
|
|
76
|
+
if ((config as any).debug) {
|
|
77
|
+
// Lazy import to avoid top-level cycles
|
|
78
|
+
const { logger } = await import('./logger.js');
|
|
79
|
+
logger.setDebug(true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate config (except for help command which doesn't need it)
|
|
83
|
+
if (!config.baseUrl || !config.username || !config.password || !config.spaceKey) {
|
|
84
|
+
console.error('Error: Missing required configuration.\n');
|
|
85
|
+
console.error('Please provide all required options or set environment variables.');
|
|
86
|
+
console.error('Run with --help for usage information.\n');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const executor = new CommandExecutor(config);
|
|
91
|
+
|
|
92
|
+
// Validate commands
|
|
93
|
+
let requestedCommands: Awaited<ReturnType<typeof executor.validateCommands>>;
|
|
94
|
+
try {
|
|
95
|
+
requestedCommands = executor.validateCommands(commands);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`Error: ${error instanceof Error ? error.message : error}\n`);
|
|
98
|
+
const helpCommand = new HelpCommand();
|
|
99
|
+
await helpCommand.execute({ config: {} as ConfluenceConfig, args });
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle help command
|
|
104
|
+
if (requestedCommands.includes('help')) {
|
|
105
|
+
const helpCommand = new HelpCommand();
|
|
106
|
+
await helpCommand.execute({ config: {} as ConfluenceConfig, args });
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const context: CommandContext = { config, args };
|
|
112
|
+
await executor.executeCommands(requestedCommands, context);
|
|
113
|
+
process.exit(0);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('\n✗ Command failed:', error instanceof Error ? error.message : error);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
main();
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export class Logger {
|
|
2
|
+
private debugEnabled: boolean;
|
|
3
|
+
private lastDebugTime: number;
|
|
4
|
+
|
|
5
|
+
// ANSI color codes
|
|
6
|
+
private colors = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
gray: '\x1b[90m',
|
|
9
|
+
cyan: '\x1b[36m',
|
|
10
|
+
yellow: '\x1b[33m',
|
|
11
|
+
red: '\x1b[31m',
|
|
12
|
+
green: '\x1b[32m',
|
|
13
|
+
blue: '\x1b[34m',
|
|
14
|
+
magenta: '\x1b[35m',
|
|
15
|
+
white: '\x1b[37m'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
constructor(debug = false) {
|
|
19
|
+
this.debugEnabled = debug || !!process.env.DEBUG;
|
|
20
|
+
this.lastDebugTime = Date.now();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
debug(...args: unknown[]) {
|
|
24
|
+
if (this.debugEnabled) {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const elapsed = now - this.lastDebugTime;
|
|
27
|
+
this.lastDebugTime = now;
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.debug(`${this.colors.cyan}[DEBUG +${elapsed}ms]${this.colors.reset}`, ...args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setDebug(enabled: boolean) {
|
|
34
|
+
this.debugEnabled = !!enabled;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
info(...args: unknown[]) {
|
|
38
|
+
// eslint-disable-next-line no-console
|
|
39
|
+
console.log(`${this.colors.blue}[INFO]${this.colors.reset}`, ...args);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
warn(...args: unknown[]) {
|
|
43
|
+
// eslint-disable-next-line no-console
|
|
44
|
+
console.warn(`${this.colors.yellow}[WARN]${this.colors.reset}`, ...args);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
error(...args: unknown[]) {
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.error(`${this.colors.red}[ERROR]${this.colors.reset}`, ...args);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
success(...args: unknown[]) {
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.log(`${this.colors.green}[SUCCESS]${this.colors.reset}`, ...args);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const logger = new Logger();
|
|
59
|
+
export { logger };
|
|
60
|
+
export default logger;
|
package/src/test.sh
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Test script for minimal Confluence exporter
|
|
4
|
+
# This script demonstrates how to test the exporter
|
|
5
|
+
|
|
6
|
+
echo "╔════════════════════════════════════════════════════╗"
|
|
7
|
+
echo "║ Testing Minimal Confluence Exporter ║"
|
|
8
|
+
echo "╚════════════════════════════════════════════════════╝"
|
|
9
|
+
echo ""
|
|
10
|
+
|
|
11
|
+
# Check if Node.js version is adequate
|
|
12
|
+
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
|
|
13
|
+
if [ "$NODE_VERSION" -lt 18 ]; then
|
|
14
|
+
echo "❌ Error: Node.js 18+ required (current: $(node --version))"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
echo "✓ Node.js version: $(node --version)"
|
|
19
|
+
echo ""
|
|
20
|
+
|
|
21
|
+
# Build the project
|
|
22
|
+
echo "📦 Building TypeScript..."
|
|
23
|
+
npm run build
|
|
24
|
+
|
|
25
|
+
if [ $? -ne 0 ]; then
|
|
26
|
+
echo "❌ Build failed!"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo "✓ Build successful"
|
|
31
|
+
echo ""
|
|
32
|
+
|
|
33
|
+
# Check if environment variables are set
|
|
34
|
+
if [ -z "$CONFLUENCE_BASE_URL" ] || [ -z "$CONFLUENCE_USERNAME" ] || [ -z "$CONFLUENCE_PASSWORD" ] || [ -z "$CONFLUENCE_SPACE_KEY" ]; then
|
|
35
|
+
echo "⚠️ Environment variables not set. Please set:"
|
|
36
|
+
echo ""
|
|
37
|
+
echo " export CONFLUENCE_BASE_URL='https://your-instance.atlassian.net'"
|
|
38
|
+
echo " export CONFLUENCE_USERNAME='your-email@example.com'"
|
|
39
|
+
echo " export CONFLUENCE_PASSWORD='your-api-token'"
|
|
40
|
+
echo " export CONFLUENCE_SPACE_KEY='YOURSPACE'"
|
|
41
|
+
echo " export OUTPUT_DIR='./test-output' # optional"
|
|
42
|
+
echo ""
|
|
43
|
+
echo "Or provide as command line arguments:"
|
|
44
|
+
echo " npm run start -- <baseUrl> <username> <password> <spaceKey> [outputDir]"
|
|
45
|
+
echo ""
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Run the exporter
|
|
50
|
+
echo "🚀 Running export..."
|
|
51
|
+
echo " Space: $CONFLUENCE_SPACE_KEY"
|
|
52
|
+
echo " Output: ${OUTPUT_DIR:-./output}"
|
|
53
|
+
echo ""
|
|
54
|
+
|
|
55
|
+
npm run start
|
|
56
|
+
|
|
57
|
+
if [ $? -eq 0 ]; then
|
|
58
|
+
echo ""
|
|
59
|
+
echo "✓ Test completed successfully!"
|
|
60
|
+
echo ""
|
|
61
|
+
echo "Check the output directory for exported markdown files."
|
|
62
|
+
else
|
|
63
|
+
echo ""
|
|
64
|
+
echo "❌ Test failed!"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type definitions for Confluence export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Configuration Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
export interface ConfluenceConfig {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
username: string;
|
|
12
|
+
password: string;
|
|
13
|
+
spaceKey: string;
|
|
14
|
+
outputDir: string;
|
|
15
|
+
pageId?: string; // Optional: if specified, export only this page
|
|
16
|
+
pageSize?: number; // Optional: number of items per API page (default: 25)
|
|
17
|
+
limit?: number; // Optional: maximum number of pages to process
|
|
18
|
+
clear?: boolean; // Optional: if specified, clears the output directory before export
|
|
19
|
+
force?: boolean; // Optional: if specified, forces re-download of all pages regardless of status
|
|
20
|
+
debug?: boolean; // Optional: enable debug logging
|
|
21
|
+
parallel?: number; // Optional: number of concurrent operations (default: 5)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Core Domain Types
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export interface Page {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
body: string;
|
|
32
|
+
version?: number;
|
|
33
|
+
parentId?: string;
|
|
34
|
+
modifiedDate?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface User {
|
|
38
|
+
userKey: string;
|
|
39
|
+
username: string;
|
|
40
|
+
displayName: string;
|
|
41
|
+
email?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// API Response Types
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
export interface PaginatedResponse<T> {
|
|
49
|
+
results: T[];
|
|
50
|
+
start: number;
|
|
51
|
+
limit: number;
|
|
52
|
+
size: number;
|
|
53
|
+
_links?: {
|
|
54
|
+
next?: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PageResponse {
|
|
59
|
+
id: string;
|
|
60
|
+
title: string;
|
|
61
|
+
body?: { storage?: { value: string } };
|
|
62
|
+
version?: { number: number; when?: string };
|
|
63
|
+
ancestors?: Array<{ id: string }>;
|
|
64
|
+
history?: { lastUpdated?: { when?: string } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RawPage {
|
|
68
|
+
id: string;
|
|
69
|
+
title: string;
|
|
70
|
+
body?: { storage?: { value: string } };
|
|
71
|
+
version?: { number: number; when?: string };
|
|
72
|
+
ancestors?: Array<{ id: string }>;
|
|
73
|
+
history?: { lastUpdated?: { when?: string } };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ListPagesResponse {
|
|
77
|
+
results: RawPage[];
|
|
78
|
+
start: number;
|
|
79
|
+
limit: number;
|
|
80
|
+
size: number;
|
|
81
|
+
_links?: {
|
|
82
|
+
next?: string;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ChildPageResponse {
|
|
87
|
+
id: string;
|
|
88
|
+
title: string;
|
|
89
|
+
version?: { number: number };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ChildPagesResponse {
|
|
93
|
+
results: ChildPageResponse[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface AttachmentResult {
|
|
97
|
+
id: string;
|
|
98
|
+
title: string;
|
|
99
|
+
_links: {
|
|
100
|
+
download: string;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface AttachmentResponse {
|
|
105
|
+
results: AttachmentResult[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Index & Export Types
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
export interface PageMetadata {
|
|
113
|
+
id: string;
|
|
114
|
+
title: string;
|
|
115
|
+
version?: number;
|
|
116
|
+
parentId?: string;
|
|
117
|
+
modifiedDate?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface PageIndexEntry {
|
|
121
|
+
id: string;
|
|
122
|
+
title: string;
|
|
123
|
+
version?: number;
|
|
124
|
+
parentId?: string;
|
|
125
|
+
modifiedDate?: string;
|
|
126
|
+
indexedDate: string;
|
|
127
|
+
pageNumber: number;
|
|
128
|
+
downloadedVersion?: number; // Last downloaded version
|
|
129
|
+
downloadedAt?: string; // Last download timestamp (ISO 8601)
|
|
130
|
+
queueReason?: 'new' | 'updated';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface PageIndex {
|
|
134
|
+
spaceKey: string;
|
|
135
|
+
exportDate: string;
|
|
136
|
+
totalPages: number;
|
|
137
|
+
pages: PageIndexEntry[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface PageTreeNode {
|
|
141
|
+
id: string;
|
|
142
|
+
title: string;
|
|
143
|
+
version?: number;
|
|
144
|
+
parentId?: string;
|
|
145
|
+
modifiedDate?: string;
|
|
146
|
+
children?: PageTreeNode[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Transformation Types
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
export interface MarkdownResult {
|
|
154
|
+
content: string;
|
|
155
|
+
frontMatter: {
|
|
156
|
+
title: string;
|
|
157
|
+
id: string;
|
|
158
|
+
version?: number;
|
|
159
|
+
parentId?: string;
|
|
160
|
+
};
|
|
161
|
+
images: Array<{
|
|
162
|
+
filename: string;
|
|
163
|
+
data: Buffer;
|
|
164
|
+
}>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Metadata sidecar file for tracking download state
|
|
169
|
+
* Stored as .meta.json alongside each .html file
|
|
170
|
+
*/
|
|
171
|
+
export interface PageMeta {
|
|
172
|
+
pageId: string;
|
|
173
|
+
version: number;
|
|
174
|
+
modifiedDate: string;
|
|
175
|
+
downloadedAt: string;
|
|
176
|
+
}
|