@moxn/kb-migrate 0.1.1 → 0.2.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/dist/client.d.ts +15 -2
- package/dist/client.js +64 -2
- package/dist/export.d.ts +8 -0
- package/dist/export.js +269 -0
- package/dist/index.js +65 -0
- package/dist/types.d.ts +101 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API client for Moxn KB
|
|
3
3
|
*/
|
|
4
|
-
import type { ExtractedDocument, MigrationOptions, MigrationResult } from './types.js';
|
|
4
|
+
import type { ExtractedDocument, MigrationOptions, MigrationResult, DocumentListItem, DocumentDetail, ExportOptions } from './types.js';
|
|
5
5
|
export declare class MoxnClient {
|
|
6
6
|
private apiUrl;
|
|
7
7
|
private apiKey;
|
|
8
8
|
private defaultPermission?;
|
|
9
9
|
private aiAccess?;
|
|
10
|
-
constructor(options: MigrationOptions);
|
|
10
|
+
constructor(options: MigrationOptions | ExportOptions);
|
|
11
11
|
/**
|
|
12
12
|
* Migrate a single document
|
|
13
13
|
*/
|
|
14
14
|
migrateDocument(doc: ExtractedDocument, basePath: string, onConflict: 'skip' | 'update', dryRun: boolean): Promise<MigrationResult>;
|
|
15
|
+
/**
|
|
16
|
+
* List all documents, optionally filtered by path prefix.
|
|
17
|
+
* Handles pagination automatically.
|
|
18
|
+
*/
|
|
19
|
+
listDocuments(pathPrefix?: string): Promise<DocumentListItem[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Get full document detail with sections and content.
|
|
22
|
+
*/
|
|
23
|
+
getDocument(documentId: string): Promise<DocumentDetail>;
|
|
24
|
+
/**
|
|
25
|
+
* Download a file from a URL to a local path.
|
|
26
|
+
*/
|
|
27
|
+
downloadFile(url: string, destPath: string): Promise<void>;
|
|
15
28
|
private buildPath;
|
|
16
29
|
private processSections;
|
|
17
30
|
private processContentBlocks;
|
package/dist/client.js
CHANGED
|
@@ -10,8 +10,12 @@ export class MoxnClient {
|
|
|
10
10
|
constructor(options) {
|
|
11
11
|
this.apiUrl = options.apiUrl.replace(/\/$/, '');
|
|
12
12
|
this.apiKey = options.apiKey;
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if ('defaultPermission' in options) {
|
|
14
|
+
this.defaultPermission = options.defaultPermission;
|
|
15
|
+
}
|
|
16
|
+
if ('aiAccess' in options) {
|
|
17
|
+
this.aiAccess = options.aiAccess;
|
|
18
|
+
}
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
21
|
* Migrate a single document
|
|
@@ -101,6 +105,64 @@ export class MoxnClient {
|
|
|
101
105
|
};
|
|
102
106
|
}
|
|
103
107
|
}
|
|
108
|
+
// ──────────────────────────────────────────────
|
|
109
|
+
// Export methods
|
|
110
|
+
// ──────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* List all documents, optionally filtered by path prefix.
|
|
113
|
+
* Handles pagination automatically.
|
|
114
|
+
*/
|
|
115
|
+
async listDocuments(pathPrefix) {
|
|
116
|
+
const allDocs = [];
|
|
117
|
+
let offset = 0;
|
|
118
|
+
const limit = 100;
|
|
119
|
+
while (true) {
|
|
120
|
+
const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
|
|
121
|
+
if (pathPrefix) {
|
|
122
|
+
params.set('path', pathPrefix);
|
|
123
|
+
}
|
|
124
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents?${params}`, {
|
|
125
|
+
headers: { 'x-api-key': this.apiKey },
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const error = await response.text();
|
|
129
|
+
throw new Error(`Failed to list documents: ${response.status} ${error}`);
|
|
130
|
+
}
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
allDocs.push(...data.documents);
|
|
133
|
+
if (!data.pagination.hasMore)
|
|
134
|
+
break;
|
|
135
|
+
offset += limit;
|
|
136
|
+
}
|
|
137
|
+
return allDocs;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get full document detail with sections and content.
|
|
141
|
+
*/
|
|
142
|
+
async getDocument(documentId) {
|
|
143
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}`, {
|
|
144
|
+
headers: { 'x-api-key': this.apiKey },
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const error = await response.text();
|
|
148
|
+
throw new Error(`Failed to get document ${documentId}: ${response.status} ${error}`);
|
|
149
|
+
}
|
|
150
|
+
return response.json();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Download a file from a URL to a local path.
|
|
154
|
+
*/
|
|
155
|
+
async downloadFile(url, destPath) {
|
|
156
|
+
const response = await fetch(url);
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw new Error(`Failed to download ${url}: ${response.status}`);
|
|
159
|
+
}
|
|
160
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
161
|
+
await fs.writeFile(destPath, buffer);
|
|
162
|
+
}
|
|
163
|
+
// ──────────────────────────────────────────────
|
|
164
|
+
// Migration methods
|
|
165
|
+
// ──────────────────────────────────────────────
|
|
104
166
|
buildPath(basePath, relativePath) {
|
|
105
167
|
const base = basePath.replace(/^\/+|\/+$/g, '');
|
|
106
168
|
const rel = relativePath.replace(/^\/+|\/+$/g, '');
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export runner for Moxn KB
|
|
3
|
+
*
|
|
4
|
+
* Exports documents from Moxn Knowledge Base to local markdown files
|
|
5
|
+
* with downloaded media assets (images, PDFs, CSVs).
|
|
6
|
+
*/
|
|
7
|
+
import type { ExportOptions, ExportLog } from './types.js';
|
|
8
|
+
export declare function runExport(outputDir: string, options: ExportOptions): Promise<ExportLog>;
|
package/dist/export.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export runner for Moxn KB
|
|
3
|
+
*
|
|
4
|
+
* Exports documents from Moxn Knowledge Base to local markdown files
|
|
5
|
+
* with downloaded media assets (images, PDFs, CSVs).
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { MoxnClient } from './client.js';
|
|
10
|
+
// ──────────────────────────────────────────────
|
|
11
|
+
// Utility functions
|
|
12
|
+
// ──────────────────────────────────────────────
|
|
13
|
+
/** Strip <moxn:comment> tags from text, preserving the inner text. */
|
|
14
|
+
function stripCommentTags(text) {
|
|
15
|
+
return text.replace(/<moxn:comment[^>]*>([\s\S]*?)<\/moxn:comment>/g, '$1');
|
|
16
|
+
}
|
|
17
|
+
/** Derive a local filename from a storage key or URL. */
|
|
18
|
+
function deriveFilename(storageKey, url, fallbackExt) {
|
|
19
|
+
if (storageKey) {
|
|
20
|
+
const parts = storageKey.split('/');
|
|
21
|
+
return parts[parts.length - 1];
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const urlPath = new URL(url).pathname;
|
|
25
|
+
const basename = urlPath.split('/').pop();
|
|
26
|
+
if (basename && basename.includes('.')) {
|
|
27
|
+
return basename;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Not a valid URL
|
|
32
|
+
}
|
|
33
|
+
return `export-${Date.now()}${fallbackExt}`;
|
|
34
|
+
}
|
|
35
|
+
/** Get file extension from MIME type. */
|
|
36
|
+
function mimeToExt(mimeType) {
|
|
37
|
+
const map = {
|
|
38
|
+
'image/png': '.png',
|
|
39
|
+
'image/jpeg': '.jpg',
|
|
40
|
+
'image/gif': '.gif',
|
|
41
|
+
'image/webp': '.webp',
|
|
42
|
+
'application/pdf': '.pdf',
|
|
43
|
+
'text/csv': '.csv',
|
|
44
|
+
};
|
|
45
|
+
return map[mimeType] || '';
|
|
46
|
+
}
|
|
47
|
+
/** Convert a document path to a local file path. */
|
|
48
|
+
function docPathToFilePath(docPath) {
|
|
49
|
+
return `${docPath.replace(/^\//, '')}.md`;
|
|
50
|
+
}
|
|
51
|
+
// ──────────────────────────────────────────────
|
|
52
|
+
// Markdown builder
|
|
53
|
+
// ──────────────────────────────────────────────
|
|
54
|
+
function buildMarkdown(doc, mediaMap, mdFilePath) {
|
|
55
|
+
const lines = [];
|
|
56
|
+
lines.push(`# ${doc.name}`);
|
|
57
|
+
lines.push('');
|
|
58
|
+
if (doc.description) {
|
|
59
|
+
lines.push(doc.description);
|
|
60
|
+
lines.push('');
|
|
61
|
+
}
|
|
62
|
+
for (const section of doc.sections) {
|
|
63
|
+
lines.push(`## ${section.name}`);
|
|
64
|
+
lines.push('');
|
|
65
|
+
for (const block of section.content) {
|
|
66
|
+
if (block.blockType === 'text' && block.text) {
|
|
67
|
+
lines.push(stripCommentTags(block.text));
|
|
68
|
+
lines.push('');
|
|
69
|
+
}
|
|
70
|
+
else if (block.blockType === 'image' && block.url) {
|
|
71
|
+
const key = block.storageKey || block.url;
|
|
72
|
+
const localPath = mediaMap.get(key);
|
|
73
|
+
if (localPath) {
|
|
74
|
+
const relativePath = path.relative(path.dirname(mdFilePath), localPath);
|
|
75
|
+
lines.push(``);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
lines.push(``);
|
|
79
|
+
}
|
|
80
|
+
lines.push('');
|
|
81
|
+
}
|
|
82
|
+
else if (block.blockType === 'document' && block.url) {
|
|
83
|
+
const key = block.storageKey || block.url;
|
|
84
|
+
const localPath = mediaMap.get(key);
|
|
85
|
+
if (localPath) {
|
|
86
|
+
const relativePath = path.relative(path.dirname(mdFilePath), localPath);
|
|
87
|
+
lines.push(`[${block.filename || 'document'}](${relativePath})`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
lines.push(`[${block.filename || 'document'}](${block.url})`);
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
else if (block.blockType === 'csv' && block.url) {
|
|
95
|
+
const key = block.storageKey || block.url;
|
|
96
|
+
const localPath = mediaMap.get(key);
|
|
97
|
+
if (localPath) {
|
|
98
|
+
const relativePath = path.relative(path.dirname(mdFilePath), localPath);
|
|
99
|
+
lines.push(`[${block.filename || 'data.csv'}](${relativePath})`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
lines.push(`[${block.filename || 'data.csv'}](${block.url})`);
|
|
103
|
+
}
|
|
104
|
+
lines.push('');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Trim trailing blank lines, end with single newline
|
|
109
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
110
|
+
lines.pop();
|
|
111
|
+
}
|
|
112
|
+
lines.push('');
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
// ──────────────────────────────────────────────
|
|
116
|
+
// Media helpers
|
|
117
|
+
// ──────────────────────────────────────────────
|
|
118
|
+
function getMediaTarget(block, options) {
|
|
119
|
+
if (block.blockType === 'image') {
|
|
120
|
+
return { targetDir: options.imageDir, fallbackExt: mimeToExt(block.mimeType || 'image/png') };
|
|
121
|
+
}
|
|
122
|
+
else if (block.blockType === 'document') {
|
|
123
|
+
return {
|
|
124
|
+
targetDir: options.pdfDir,
|
|
125
|
+
fallbackExt: mimeToExt(block.mimeType || 'application/pdf'),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
else if (block.blockType === 'csv') {
|
|
129
|
+
return { targetDir: options.csvDir, fallbackExt: '.csv' };
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// ──────────────────────────────────────────────
|
|
134
|
+
// Main export runner
|
|
135
|
+
// ──────────────────────────────────────────────
|
|
136
|
+
export async function runExport(outputDir, options) {
|
|
137
|
+
const startTime = Date.now();
|
|
138
|
+
const client = new MoxnClient(options);
|
|
139
|
+
// List documents
|
|
140
|
+
console.log('Fetching document list...');
|
|
141
|
+
const documents = await client.listDocuments(options.basePath || undefined);
|
|
142
|
+
console.log(`Found ${documents.length} documents`);
|
|
143
|
+
if (documents.length === 0) {
|
|
144
|
+
return {
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
sourceApi: options.apiUrl,
|
|
147
|
+
outputDir: path.resolve(outputDir),
|
|
148
|
+
basePath: options.basePath || '/',
|
|
149
|
+
options: {
|
|
150
|
+
dryRun: options.dryRun,
|
|
151
|
+
imageDir: options.imageDir,
|
|
152
|
+
pdfDir: options.pdfDir,
|
|
153
|
+
csvDir: options.csvDir,
|
|
154
|
+
},
|
|
155
|
+
results: [],
|
|
156
|
+
summary: {
|
|
157
|
+
total: 0,
|
|
158
|
+
exported: 0,
|
|
159
|
+
failed: 0,
|
|
160
|
+
mediaDownloaded: 0,
|
|
161
|
+
duration: Date.now() - startTime,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// Create output directories
|
|
166
|
+
if (!options.dryRun) {
|
|
167
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
168
|
+
fs.mkdirSync(path.join(outputDir, options.imageDir), { recursive: true });
|
|
169
|
+
fs.mkdirSync(path.join(outputDir, options.pdfDir), { recursive: true });
|
|
170
|
+
fs.mkdirSync(path.join(outputDir, options.csvDir), { recursive: true });
|
|
171
|
+
}
|
|
172
|
+
const results = [];
|
|
173
|
+
let totalMediaDownloaded = 0;
|
|
174
|
+
for (const docItem of documents) {
|
|
175
|
+
const docStart = Date.now();
|
|
176
|
+
const mediaFiles = [];
|
|
177
|
+
console.log(`Processing: ${docItem.path}`);
|
|
178
|
+
try {
|
|
179
|
+
const doc = await client.getDocument(docItem.id);
|
|
180
|
+
const mdRelativePath = docPathToFilePath(doc.path);
|
|
181
|
+
const mdFullPath = path.join(outputDir, mdRelativePath);
|
|
182
|
+
const mediaMap = new Map();
|
|
183
|
+
// Download media
|
|
184
|
+
for (const section of doc.sections) {
|
|
185
|
+
for (const block of section.content) {
|
|
186
|
+
if (block.blockType === 'text')
|
|
187
|
+
continue;
|
|
188
|
+
if (!block.url)
|
|
189
|
+
continue;
|
|
190
|
+
if (block.url.startsWith('data:'))
|
|
191
|
+
continue;
|
|
192
|
+
const target = getMediaTarget(block, options);
|
|
193
|
+
if (!target)
|
|
194
|
+
continue;
|
|
195
|
+
const filename = deriveFilename(block.storageKey, block.url, target.fallbackExt);
|
|
196
|
+
const localRelativePath = path.join(target.targetDir, filename);
|
|
197
|
+
const localFullPath = path.join(outputDir, localRelativePath);
|
|
198
|
+
const mapKey = block.storageKey || block.url;
|
|
199
|
+
mediaMap.set(mapKey, path.join(outputDir, localRelativePath));
|
|
200
|
+
if (!options.dryRun) {
|
|
201
|
+
if (!fs.existsSync(localFullPath)) {
|
|
202
|
+
try {
|
|
203
|
+
await client.downloadFile(block.url, localFullPath);
|
|
204
|
+
totalMediaDownloaded++;
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
console.log(` \u2717 Download failed: ${filename}: ${msg}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
totalMediaDownloaded++;
|
|
214
|
+
}
|
|
215
|
+
mediaFiles.push(localRelativePath);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const markdown = buildMarkdown(doc, mediaMap, mdFullPath);
|
|
219
|
+
if (!options.dryRun) {
|
|
220
|
+
fs.mkdirSync(path.dirname(mdFullPath), { recursive: true });
|
|
221
|
+
fs.writeFileSync(mdFullPath, markdown, 'utf-8');
|
|
222
|
+
}
|
|
223
|
+
console.log(` \u2713 ${mdRelativePath} (${doc.sections.length} sections, ${mediaFiles.length} media)`);
|
|
224
|
+
results.push({
|
|
225
|
+
documentId: doc.id,
|
|
226
|
+
documentPath: doc.path,
|
|
227
|
+
outputFile: mdRelativePath,
|
|
228
|
+
status: 'exported',
|
|
229
|
+
sectionsCount: doc.sections.length,
|
|
230
|
+
mediaFiles,
|
|
231
|
+
duration: Date.now() - docStart,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
236
|
+
console.log(` \u2717 Failed: ${message}`);
|
|
237
|
+
results.push({
|
|
238
|
+
documentId: docItem.id,
|
|
239
|
+
documentPath: docItem.path,
|
|
240
|
+
outputFile: docPathToFilePath(docItem.path),
|
|
241
|
+
status: 'failed',
|
|
242
|
+
sectionsCount: 0,
|
|
243
|
+
mediaFiles: [],
|
|
244
|
+
error: message,
|
|
245
|
+
duration: Date.now() - docStart,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
timestamp: new Date().toISOString(),
|
|
251
|
+
sourceApi: options.apiUrl,
|
|
252
|
+
outputDir: path.resolve(outputDir),
|
|
253
|
+
basePath: options.basePath || '/',
|
|
254
|
+
options: {
|
|
255
|
+
dryRun: options.dryRun,
|
|
256
|
+
imageDir: options.imageDir,
|
|
257
|
+
pdfDir: options.pdfDir,
|
|
258
|
+
csvDir: options.csvDir,
|
|
259
|
+
},
|
|
260
|
+
results,
|
|
261
|
+
summary: {
|
|
262
|
+
total: results.length,
|
|
263
|
+
exported: results.filter((r) => r.status === 'exported').length,
|
|
264
|
+
failed: results.filter((r) => r.status === 'failed').length,
|
|
265
|
+
mediaDownloaded: totalMediaDownloaded,
|
|
266
|
+
duration: Date.now() - startTime,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { Command } from 'commander';
|
|
16
16
|
import { LocalSource } from './sources/local.js';
|
|
17
17
|
import { MoxnClient } from './client.js';
|
|
18
|
+
import { runExport } from './export.js';
|
|
18
19
|
const DEFAULT_API_URL = 'https://moxn.dev';
|
|
19
20
|
const DEFAULT_EXTENSIONS = ['.md', '.txt'];
|
|
20
21
|
async function runMigration(source, options) {
|
|
@@ -91,6 +92,27 @@ function printSummary(log) {
|
|
|
91
92
|
console.log('\n(Dry run - no changes made)');
|
|
92
93
|
}
|
|
93
94
|
}
|
|
95
|
+
function printExportSummary(log) {
|
|
96
|
+
console.log('\n--- Export Summary ---');
|
|
97
|
+
console.log(`Source: ${log.sourceApi}`);
|
|
98
|
+
console.log(`Target: ${log.outputDir}`);
|
|
99
|
+
console.log(`Base path: ${log.basePath}`);
|
|
100
|
+
console.log(`Duration: ${(log.summary.duration / 1000).toFixed(1)}s`);
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(`Total: ${log.summary.total}`);
|
|
103
|
+
console.log(`Exported: ${log.summary.exported}`);
|
|
104
|
+
console.log(`Failed: ${log.summary.failed}`);
|
|
105
|
+
console.log(`Media: ${log.summary.mediaDownloaded}`);
|
|
106
|
+
if (log.summary.failed > 0) {
|
|
107
|
+
console.log('\nFailed documents:');
|
|
108
|
+
for (const f of log.results.filter((r) => r.status === 'failed')) {
|
|
109
|
+
console.log(` - ${f.documentPath}: ${f.error}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (log.options.dryRun) {
|
|
113
|
+
console.log('\n(Dry run - no changes made)');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
94
116
|
const program = new Command();
|
|
95
117
|
program
|
|
96
118
|
.name('moxn-kb-migrate')
|
|
@@ -151,6 +173,49 @@ program
|
|
|
151
173
|
process.exit(1);
|
|
152
174
|
}
|
|
153
175
|
});
|
|
176
|
+
program
|
|
177
|
+
.command('export <directory>')
|
|
178
|
+
.description('Export documents from Moxn Knowledge Base to local files')
|
|
179
|
+
.option('--api-key <key>', 'API key (or set MOXN_API_KEY env var)')
|
|
180
|
+
.option('--api-url <url>', 'API base URL', DEFAULT_API_URL)
|
|
181
|
+
.option('--base-path <path>', 'Only export docs under this path prefix', '/')
|
|
182
|
+
.option('--image-dir <name>', 'Directory name for images', 'images')
|
|
183
|
+
.option('--pdf-dir <name>', 'Directory name for PDFs', 'pdfs')
|
|
184
|
+
.option('--csv-dir <name>', 'Directory name for CSVs', 'csvs')
|
|
185
|
+
.option('--dry-run', 'Preview without writing files', false)
|
|
186
|
+
.option('--json', 'Output results as JSON', false)
|
|
187
|
+
.action(async (directory, opts) => {
|
|
188
|
+
const apiKey = opts.apiKey || process.env.MOXN_API_KEY;
|
|
189
|
+
if (!apiKey) {
|
|
190
|
+
console.error('Error: API key required. Use --api-key or set MOXN_API_KEY env var.');
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
const exportOptions = {
|
|
194
|
+
apiUrl: opts.apiUrl,
|
|
195
|
+
apiKey,
|
|
196
|
+
basePath: opts.basePath,
|
|
197
|
+
imageDir: opts.imageDir,
|
|
198
|
+
pdfDir: opts.pdfDir,
|
|
199
|
+
csvDir: opts.csvDir,
|
|
200
|
+
dryRun: opts.dryRun,
|
|
201
|
+
};
|
|
202
|
+
try {
|
|
203
|
+
const log = await runExport(directory, exportOptions);
|
|
204
|
+
if (opts.json) {
|
|
205
|
+
console.log(JSON.stringify(log, null, 2));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
printExportSummary(log);
|
|
209
|
+
}
|
|
210
|
+
if (log.summary.failed > 0) {
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error('Export failed:', error instanceof Error ? error.message : error);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
154
219
|
// Future: Add notion and google-docs commands here
|
|
155
220
|
// program
|
|
156
221
|
// .command('notion')
|
package/dist/types.d.ts
CHANGED
|
@@ -107,3 +107,104 @@ export interface ConflictError {
|
|
|
107
107
|
documentId: string;
|
|
108
108
|
branchId: string;
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* A content block as returned by the KB API
|
|
112
|
+
*/
|
|
113
|
+
export interface ExportContentBlock {
|
|
114
|
+
blockType: 'text' | 'image' | 'document' | 'csv';
|
|
115
|
+
text?: string;
|
|
116
|
+
url?: string;
|
|
117
|
+
mimeType?: string;
|
|
118
|
+
alt?: string;
|
|
119
|
+
filename?: string;
|
|
120
|
+
storageKey?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A section within a document (API response)
|
|
124
|
+
*/
|
|
125
|
+
export interface DocumentSection {
|
|
126
|
+
id: string;
|
|
127
|
+
name: string;
|
|
128
|
+
position: number;
|
|
129
|
+
content: ExportContentBlock[];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A document in the list response
|
|
133
|
+
*/
|
|
134
|
+
export interface DocumentListItem {
|
|
135
|
+
id: string;
|
|
136
|
+
path: string;
|
|
137
|
+
name: string;
|
|
138
|
+
description: string | null;
|
|
139
|
+
createdAt: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Full document detail with sections
|
|
143
|
+
*/
|
|
144
|
+
export interface DocumentDetail {
|
|
145
|
+
id: string;
|
|
146
|
+
path: string;
|
|
147
|
+
name: string;
|
|
148
|
+
description: string | null;
|
|
149
|
+
sections: DocumentSection[];
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Paginated list response from KB API
|
|
153
|
+
*/
|
|
154
|
+
export interface ListResponse {
|
|
155
|
+
documents: DocumentListItem[];
|
|
156
|
+
pagination: {
|
|
157
|
+
total: number;
|
|
158
|
+
limit: number;
|
|
159
|
+
offset: number;
|
|
160
|
+
hasMore: boolean;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Result of exporting a single document
|
|
165
|
+
*/
|
|
166
|
+
export interface ExportResult {
|
|
167
|
+
documentId: string;
|
|
168
|
+
documentPath: string;
|
|
169
|
+
outputFile: string;
|
|
170
|
+
status: 'exported' | 'failed';
|
|
171
|
+
sectionsCount: number;
|
|
172
|
+
mediaFiles: string[];
|
|
173
|
+
error?: string;
|
|
174
|
+
duration: number;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Complete export log for JSON output
|
|
178
|
+
*/
|
|
179
|
+
export interface ExportLog {
|
|
180
|
+
timestamp: string;
|
|
181
|
+
sourceApi: string;
|
|
182
|
+
outputDir: string;
|
|
183
|
+
basePath: string;
|
|
184
|
+
options: {
|
|
185
|
+
dryRun: boolean;
|
|
186
|
+
imageDir: string;
|
|
187
|
+
pdfDir: string;
|
|
188
|
+
csvDir: string;
|
|
189
|
+
};
|
|
190
|
+
results: ExportResult[];
|
|
191
|
+
summary: {
|
|
192
|
+
total: number;
|
|
193
|
+
exported: number;
|
|
194
|
+
failed: number;
|
|
195
|
+
mediaDownloaded: number;
|
|
196
|
+
duration: number;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Options for export
|
|
201
|
+
*/
|
|
202
|
+
export interface ExportOptions {
|
|
203
|
+
apiUrl: string;
|
|
204
|
+
apiKey: string;
|
|
205
|
+
basePath: string;
|
|
206
|
+
imageDir: string;
|
|
207
|
+
pdfDir: string;
|
|
208
|
+
csvDir: string;
|
|
209
|
+
dryRun: boolean;
|
|
210
|
+
}
|
package/package.json
CHANGED