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,167 @@
1
+ /**
2
+ * Minimal type definitions for Confluence export
3
+ */
4
+ export interface ConfluenceConfig {
5
+ baseUrl: string;
6
+ username: string;
7
+ password: string;
8
+ spaceKey: string;
9
+ outputDir: string;
10
+ pageId?: string;
11
+ pageSize?: number;
12
+ limit?: number;
13
+ clear?: boolean;
14
+ force?: boolean;
15
+ debug?: boolean;
16
+ parallel?: number;
17
+ }
18
+ export interface Page {
19
+ id: string;
20
+ title: string;
21
+ body: string;
22
+ version?: number;
23
+ parentId?: string;
24
+ modifiedDate?: string;
25
+ }
26
+ export interface User {
27
+ userKey: string;
28
+ username: string;
29
+ displayName: string;
30
+ email?: string;
31
+ }
32
+ export interface PaginatedResponse<T> {
33
+ results: T[];
34
+ start: number;
35
+ limit: number;
36
+ size: number;
37
+ _links?: {
38
+ next?: string;
39
+ };
40
+ }
41
+ export interface PageResponse {
42
+ id: string;
43
+ title: string;
44
+ body?: {
45
+ storage?: {
46
+ value: string;
47
+ };
48
+ };
49
+ version?: {
50
+ number: number;
51
+ when?: string;
52
+ };
53
+ ancestors?: Array<{
54
+ id: string;
55
+ }>;
56
+ history?: {
57
+ lastUpdated?: {
58
+ when?: string;
59
+ };
60
+ };
61
+ }
62
+ export interface RawPage {
63
+ id: string;
64
+ title: string;
65
+ body?: {
66
+ storage?: {
67
+ value: string;
68
+ };
69
+ };
70
+ version?: {
71
+ number: number;
72
+ when?: string;
73
+ };
74
+ ancestors?: Array<{
75
+ id: string;
76
+ }>;
77
+ history?: {
78
+ lastUpdated?: {
79
+ when?: string;
80
+ };
81
+ };
82
+ }
83
+ export interface ListPagesResponse {
84
+ results: RawPage[];
85
+ start: number;
86
+ limit: number;
87
+ size: number;
88
+ _links?: {
89
+ next?: string;
90
+ };
91
+ }
92
+ export interface ChildPageResponse {
93
+ id: string;
94
+ title: string;
95
+ version?: {
96
+ number: number;
97
+ };
98
+ }
99
+ export interface ChildPagesResponse {
100
+ results: ChildPageResponse[];
101
+ }
102
+ export interface AttachmentResult {
103
+ id: string;
104
+ title: string;
105
+ _links: {
106
+ download: string;
107
+ };
108
+ }
109
+ export interface AttachmentResponse {
110
+ results: AttachmentResult[];
111
+ }
112
+ export interface PageMetadata {
113
+ id: string;
114
+ title: string;
115
+ version?: number;
116
+ parentId?: string;
117
+ modifiedDate?: string;
118
+ }
119
+ export interface PageIndexEntry {
120
+ id: string;
121
+ title: string;
122
+ version?: number;
123
+ parentId?: string;
124
+ modifiedDate?: string;
125
+ indexedDate: string;
126
+ pageNumber: number;
127
+ downloadedVersion?: number;
128
+ downloadedAt?: string;
129
+ queueReason?: 'new' | 'updated';
130
+ }
131
+ export interface PageIndex {
132
+ spaceKey: string;
133
+ exportDate: string;
134
+ totalPages: number;
135
+ pages: PageIndexEntry[];
136
+ }
137
+ export interface PageTreeNode {
138
+ id: string;
139
+ title: string;
140
+ version?: number;
141
+ parentId?: string;
142
+ modifiedDate?: string;
143
+ children?: PageTreeNode[];
144
+ }
145
+ export interface MarkdownResult {
146
+ content: string;
147
+ frontMatter: {
148
+ title: string;
149
+ id: string;
150
+ version?: number;
151
+ parentId?: string;
152
+ };
153
+ images: Array<{
154
+ filename: string;
155
+ data: Buffer;
156
+ }>;
157
+ }
158
+ /**
159
+ * Metadata sidecar file for tracking download state
160
+ * Stored as .meta.json alongside each .html file
161
+ */
162
+ export interface PageMeta {
163
+ pageId: string;
164
+ version: number;
165
+ modifiedDate: string;
166
+ downloadedAt: string;
167
+ }
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Minimal type definitions for Confluence export
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,56 @@
1
+ import type { ConfluenceConfig, PageIndexEntry } from './types.js';
2
+ /**
3
+ * Utility functions used across the application
4
+ */
5
+ /**
6
+ * Convert text to safe filename/slug
7
+ *
8
+ * @param text - Text to slugify
9
+ * @returns Slugified text (lowercase, hyphens, no special chars)
10
+ *
11
+ * @example
12
+ * slugify("My Page Title!") // "my-page-title"
13
+ */
14
+ export declare function slugify(text: string): string;
15
+ /**
16
+ * Attempt to reverse slugification (best effort)
17
+ * Converts hyphens to spaces and capitalizes first letter of each word
18
+ *
19
+ * @param slug - Slugified text to convert back
20
+ * @returns Title-cased text with spaces
21
+ *
22
+ * @example
23
+ * unslugify("my-page-title") // "My Page Title"
24
+ */
25
+ export declare function unslugify(slug: string): string;
26
+ export declare const pageFilename: (item: PageIndexEntry, ext?: string) => string;
27
+ export declare const pagePath: (id: string, config: ConfluenceConfig) => string;
28
+ /**
29
+ * Read a specific entry from _index.yaml
30
+ */
31
+ export declare function readIndexEntry(indexPath: string, pageId: string): PageIndexEntry | null;
32
+ /**
33
+ * Update a specific entry in _index.yaml
34
+ */
35
+ export declare function updateIndexEntry(indexPath: string, pageId: string, updates: Partial<PageIndexEntry>): boolean;
36
+ /**
37
+ * Find existing HTML file for a page by ID
38
+ * Searches recursively in the output directory for {pageId}-*.html pattern
39
+ *
40
+ * @param outputDir - Root directory to search
41
+ * @param pageId - Page ID to find
42
+ * @returns Path to HTML file or null if not found
43
+ */
44
+ export declare function findExistingFile(outputDir: string, pageId: string): string | null;
45
+ /**
46
+ * Check if a page needs to be downloaded based on version comparison
47
+ * Uses index entry download tracking fields
48
+ *
49
+ * @param indexEntry - Page entry from index with current version
50
+ * @returns Object with needsDownload boolean and reason
51
+ */
52
+ export declare function checkPageStatus(indexEntry: PageIndexEntry): {
53
+ needsDownload: boolean;
54
+ reason: 'new' | 'updated' | 'up-to-date';
55
+ details?: string;
56
+ };
package/dist/utils.js ADDED
@@ -0,0 +1,178 @@
1
+ import { parse, stringify } from 'yaml';
2
+ import { readFileSync, existsSync, writeFileSync, readdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ /**
5
+ * Utility functions used across the application
6
+ */
7
+ /**
8
+ * Convert text to safe filename/slug
9
+ *
10
+ * @param text - Text to slugify
11
+ * @returns Slugified text (lowercase, hyphens, no special chars)
12
+ *
13
+ * @example
14
+ * slugify("My Page Title!") // "my-page-title"
15
+ */
16
+ export function slugify(text) {
17
+ return text
18
+ .toLowerCase()
19
+ .replace(/[^\w\s-]/g, '') // Remove special chars
20
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
21
+ .replace(/-+/g, '-') // Replace multiple hyphens with single
22
+ .trim();
23
+ }
24
+ /**
25
+ * Attempt to reverse slugification (best effort)
26
+ * Converts hyphens to spaces and capitalizes first letter of each word
27
+ *
28
+ * @param slug - Slugified text to convert back
29
+ * @returns Title-cased text with spaces
30
+ *
31
+ * @example
32
+ * unslugify("my-page-title") // "My Page Title"
33
+ */
34
+ export function unslugify(slug) {
35
+ return slug
36
+ .replace(/-/g, ' ') // Replace hyphens with spaces
37
+ .replace(/\b\w/g, c => c.toUpperCase()); // Capitalize first letter of each word
38
+ }
39
+ export const pageFilename = (item, ext = '.md') => {
40
+ const slug = slugify(item.title);
41
+ return `${item.id}-${slug}${ext}`;
42
+ };
43
+ export const pagePath = (id, config) => {
44
+ const indexFile = join(config.outputDir, '_index.yaml');
45
+ const indexContent = readFileSync(indexFile, 'utf-8');
46
+ const index = parse(indexContent);
47
+ const findParents = (id, path) => {
48
+ const entry = index.find(e => e.id === id);
49
+ if (!entry)
50
+ return [];
51
+ path.unshift(pageFilename(entry, ''));
52
+ if (entry.parentId) {
53
+ return findParents(entry.parentId, path);
54
+ }
55
+ return path;
56
+ };
57
+ const item = index.find(e => e.id === id);
58
+ if (!item) {
59
+ throw new Error(`Page with ID ${id} not found in index`);
60
+ }
61
+ const parents = item.parentId ? findParents(item.parentId, []) : [];
62
+ return join(config.outputDir, config.spaceKey, ...parents, pageFilename(item, '.html'));
63
+ };
64
+ // ============================================================================
65
+ // Page Metadata Utilities (Index-based)
66
+ // ============================================================================
67
+ /**
68
+ * Read a specific entry from _index.yaml
69
+ */
70
+ export function readIndexEntry(indexPath, pageId) {
71
+ if (!existsSync(indexPath))
72
+ return null;
73
+ try {
74
+ const content = readFileSync(indexPath, 'utf-8');
75
+ const index = parse(content);
76
+ return index.find(entry => entry.id === pageId) || null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Update a specific entry in _index.yaml
84
+ */
85
+ export function updateIndexEntry(indexPath, pageId, updates) {
86
+ if (!existsSync(indexPath))
87
+ return false;
88
+ try {
89
+ const content = readFileSync(indexPath, 'utf-8');
90
+ const index = parse(content);
91
+ const entryIndex = index.findIndex(entry => entry.id === pageId);
92
+ if (entryIndex === -1)
93
+ return false;
94
+ // Update the entry
95
+ index[entryIndex] = { ...index[entryIndex], ...updates };
96
+ // Write back to file
97
+ const yamlContent = stringify(index, {
98
+ indent: 2,
99
+ lineWidth: 0
100
+ });
101
+ writeFileSync(indexPath, yamlContent, 'utf-8');
102
+ return true;
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
108
+ /**
109
+ * Find existing HTML file for a page by ID
110
+ * Searches recursively in the output directory for {pageId}-*.html pattern
111
+ *
112
+ * @param outputDir - Root directory to search
113
+ * @param pageId - Page ID to find
114
+ * @returns Path to HTML file or null if not found
115
+ */
116
+ export function findExistingFile(outputDir, pageId) {
117
+ const pattern = new RegExp(`^${pageId}-.*\\.html$`);
118
+ function searchDir(dir) {
119
+ if (!existsSync(dir))
120
+ return null;
121
+ try {
122
+ const entries = readdirSync(dir, { withFileTypes: true });
123
+ for (const entry of entries) {
124
+ const fullPath = join(dir, entry.name);
125
+ if (entry.isFile() && pattern.test(entry.name)) {
126
+ return fullPath;
127
+ }
128
+ if (entry.isDirectory() && !entry.name.startsWith('_') && entry.name !== 'images') {
129
+ const found = searchDir(fullPath);
130
+ if (found)
131
+ return found;
132
+ }
133
+ }
134
+ }
135
+ catch {
136
+ // Ignore permission errors, etc.
137
+ }
138
+ return null;
139
+ }
140
+ return searchDir(outputDir);
141
+ }
142
+ /**
143
+ * Check if a page needs to be downloaded based on version comparison
144
+ * Uses index entry download tracking fields
145
+ *
146
+ * @param indexEntry - Page entry from index with current version
147
+ * @returns Object with needsDownload boolean and reason
148
+ */
149
+ export function checkPageStatus(indexEntry) {
150
+ const downloadedVersion = indexEntry.downloadedVersion ?? 0;
151
+ const currentVersion = indexEntry.version ?? 0;
152
+ // If never downloaded, it's new
153
+ if (indexEntry.downloadedAt === undefined) {
154
+ return { needsDownload: true, reason: 'new' };
155
+ }
156
+ // Compare versions (primary check)
157
+ if (currentVersion > downloadedVersion) {
158
+ return {
159
+ needsDownload: true,
160
+ reason: 'updated',
161
+ details: `v${downloadedVersion} → v${currentVersion}`
162
+ };
163
+ }
164
+ // If versions match but downloadedVersion is 0 (fallback), compare dates
165
+ if (downloadedVersion === 0 && indexEntry.modifiedDate && indexEntry.downloadedAt) {
166
+ const currentDate = new Date(indexEntry.modifiedDate);
167
+ const downloadedDate = new Date(indexEntry.downloadedAt);
168
+ if (currentDate > downloadedDate) {
169
+ return {
170
+ needsDownload: true,
171
+ reason: 'updated',
172
+ details: `modified after download`
173
+ };
174
+ }
175
+ }
176
+ return { needsDownload: false, reason: 'up-to-date' };
177
+ }
178
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import js from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+
4
+ export default [
5
+ { ignores: ['dist/**', 'node_modules/**', '**/*.cjs'] },
6
+ js.configs.recommended,
7
+ ...tseslint.configs.recommended,
8
+ {
9
+ files: ['**/*.ts'],
10
+ languageOptions: {
11
+ parser: tseslint.parser,
12
+ parserOptions: { sourceType: 'module', ecmaVersion: 'latest' }
13
+ },
14
+ rules: {
15
+ 'no-unused-vars': 'off',
16
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
17
+ 'complexity': ['warn', 10],
18
+ 'prefer-const': 'warn'
19
+ }
20
+ },
21
+ {
22
+ files: ['src/transform/cleanupRules/**/*.ts', 'src/services/markdownCleanupService.ts'],
23
+ rules: {
24
+ // Special rules for cleanup modules
25
+ 'complexity': ['error', 8], // Stricter complexity for cleanup rules
26
+ '@typescript-eslint/explicit-function-return-type': 'warn'
27
+ }
28
+ }
29
+ ];
@@ -0,0 +1,25 @@
1
+ /** @type {import('jest').Config} */
2
+ module.exports = {
3
+ preset: 'ts-jest/presets/default-esm',
4
+ testEnvironment: 'node',
5
+ roots: ['<rootDir>/tests'],
6
+ testMatch: ['**/*.test.ts'],
7
+ extensionsToTreatAsEsm: ['.ts'],
8
+ moduleNameMapper: {
9
+ '^(\\.{1,2}/.*)\\.js$': '$1',
10
+ },
11
+ transform: {
12
+ '^.+\\.ts$': ['ts-jest', {
13
+ useESM: true,
14
+ tsconfig: {
15
+ module: 'ESNext',
16
+ moduleResolution: 'node',
17
+ },
18
+ }],
19
+ },
20
+ moduleFileExtensions: ['ts', 'js', 'json'],
21
+ collectCoverageFrom: ['src/**/*.{ts,js}', '!src/**/*.test.ts'],
22
+ coverageDirectory: 'coverage',
23
+ testPathIgnorePatterns: ['/node_modules/', '/dist/'],
24
+ verbose: true,
25
+ };
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Migration script: Move .meta.json data into _index.yaml
4
+ *
5
+ * This script reads all .meta.json files in the output directory
6
+ * and updates the corresponding entries in _index.yaml with
7
+ * downloadedVersion and downloadedAt fields.
8
+ *
9
+ * Usage: node migrate-meta.js <outputDir>
10
+ */
11
+
12
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, unlinkSync } from 'fs';
13
+ import { join, dirname, basename, extname } from 'path';
14
+ import { parse, stringify } from 'yaml';
15
+
16
+ function migrateMetaToIndex(outputDir) {
17
+ const indexPath = join(outputDir, '_index.yaml');
18
+
19
+ if (!existsSync(indexPath)) {
20
+ console.error(`❌ _index.yaml not found in ${outputDir}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ console.log(`📖 Reading _index.yaml from ${indexPath}`);
25
+ const indexContent = readFileSync(indexPath, 'utf-8');
26
+ const index = parse(indexContent);
27
+
28
+ console.log(`📊 Found ${index.length} entries in index`);
29
+
30
+ let migrated = 0;
31
+ let skipped = 0;
32
+ const metaFiles = [];
33
+
34
+ // First pass: collect all .meta.json files
35
+ function collectMetaFiles(dir) {
36
+ if (!existsSync(dir)) return;
37
+
38
+ try {
39
+ const entries = readdirSync(dir, { withFileTypes: true });
40
+
41
+ for (const entry of entries) {
42
+ const fullPath = join(dir, entry.name);
43
+
44
+ if (entry.isDirectory() && !entry.name.startsWith('_') && entry.name !== 'images') {
45
+ collectMetaFiles(fullPath);
46
+ } else if (entry.isFile() && entry.name.endsWith('.meta.json')) {
47
+ metaFiles.push(fullPath);
48
+ }
49
+ }
50
+ } catch (error) {
51
+ console.warn(`⚠️ Could not read directory ${dir}:`, error);
52
+ }
53
+ }
54
+
55
+ function migrateMetaFile(metaPath) {
56
+ try {
57
+ const metaContent = readFileSync(metaPath, 'utf-8');
58
+ const meta = JSON.parse(metaContent);
59
+
60
+ // Find corresponding entry in index
61
+ const indexEntry = index.find(entry => entry.id === meta.pageId);
62
+ if (!indexEntry) {
63
+ console.log(`⚠️ No index entry found for page ${meta.pageId}, skipping`);
64
+ skipped++;
65
+ return;
66
+ }
67
+
68
+ // Update index entry with download metadata
69
+ indexEntry.downloadedVersion = meta.version;
70
+ indexEntry.downloadedAt = meta.downloadedAt;
71
+
72
+ console.log(`✅ Migrated ${meta.pageId} (${indexEntry.title}): v${meta.version} at ${meta.downloadedAt}`);
73
+
74
+ // Remove the .meta.json file
75
+ unlinkSync(metaPath);
76
+ migrated++;
77
+ } catch (error) {
78
+ console.error(`❌ Failed to migrate ${metaPath}:`, error);
79
+ }
80
+ }
81
+
82
+ function writeIndex() {
83
+ const header = `# Confluence Page Index
84
+ # Migrated download metadata from .meta.json files
85
+ # Created: ${new Date().toISOString()}
86
+
87
+ `;
88
+ const yamlContent = stringify(index, {
89
+ indent: 2,
90
+ lineWidth: 0
91
+ });
92
+
93
+ writeFileSync(indexPath, header + yamlContent, 'utf-8');
94
+ }
95
+
96
+ console.log(`\n🔍 Scanning for .meta.json files in ${outputDir}...`);
97
+ collectMetaFiles(outputDir);
98
+
99
+ if (metaFiles.length === 0) {
100
+ console.log(`ℹ️ No .meta.json files found to migrate`);
101
+ return;
102
+ }
103
+
104
+ console.log(`📋 Found ${metaFiles.length} .meta.json files to migrate`);
105
+
106
+ // Process in batches of 100
107
+ const batchSize = 100;
108
+ for (let i = 0; i < metaFiles.length; i += batchSize) {
109
+ const batch = metaFiles.slice(i, i + batchSize);
110
+ console.log(`\n🔄 Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(metaFiles.length / batchSize)} (${batch.length} files)...`);
111
+
112
+ for (const metaPath of batch) {
113
+ migrateMetaFile(metaPath);
114
+ }
115
+
116
+ console.log(`💾 Writing updated _index.yaml after batch...`);
117
+ writeIndex();
118
+ console.log(`✅ Batch complete: ${migrated} total migrated so far`);
119
+ }
120
+
121
+ console.log(`\n✅ Migration complete: ${migrated} entries updated, ${skipped} skipped`);
122
+ }
123
+ const outputDir = process.argv[2] || './output';
124
+
125
+ if (!outputDir) {
126
+ console.error('Usage: node migrate-meta.js <outputDir>');
127
+ process.exit(1);
128
+ }
129
+
130
+ console.log(`🚀 Starting migration from .meta.json to _index.yaml\n`);
131
+ migrateMetaToIndex(outputDir);
132
+ console.log(`\n🎉 Migration finished!`);
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "confluence-exporter",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Minimal standalone CLI tool to export Confluence spaces to Markdown",
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "bin": {
9
+ "confluence-export": "./dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "npm run build:tsc",
13
+ "build:tsc": "tsc -p tsconfig.json",
14
+ "start": "node dist/index.js",
15
+ "clean": "rm -rf dist",
16
+ "rebuild": "npm run clean && npm run build",
17
+ "dev": "vite-node src/index.ts",
18
+ "dev:watch": "vite-node --watch src/index.ts",
19
+ "lint": "eslint . --ext .ts",
20
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
21
+ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
22
+ "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
23
+ "typecheck": "tsc --noEmit",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
29
+ "keywords": [
30
+ "confluence",
31
+ "markdown",
32
+ "exporter",
33
+ "cli"
34
+ ],
35
+ "dependencies": {
36
+ "dotenv": "^17.2.3",
37
+ "jsdom": "^27.0.1",
38
+ "minimist": "^1.2.8",
39
+ "prettier": "^3.6.2",
40
+ "webforai": "^2.1.1",
41
+ "yaml": "^2.8.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/jest": "^30.0.0",
45
+ "@types/minimist": "^1.2.5",
46
+ "@types/node": "^20.0.0",
47
+ "jest": "^30.2.0",
48
+ "ts-jest": "^29.4.5",
49
+ "typescript": "^5.0.0",
50
+ "vite": "^7.1.10",
51
+ "vite-node": "^3.2.4"
52
+ }
53
+ }