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
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
package/dist/utils.d.ts
ADDED
|
@@ -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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"}
|
package/eslint.config.js
ADDED
|
@@ -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
|
+
];
|
package/jest.config.cjs
ADDED
|
@@ -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
|
+
};
|
package/migrate-meta.js
ADDED
|
@@ -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
|
+
}
|