nconv-cli 1.0.0 ā 1.0.3
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/.claude/settings.local.json +7 -1
- package/GITHUB_ABOUT.md +53 -0
- package/README.md +285 -131
- package/dist/index.js +1036 -40
- package/dist/index.js.map +1 -1
- package/nconv-cli-1.0.0.tgz +0 -0
- package/nconv-cli-1.0.2.tgz +0 -0
- package/nconv-cli-1.0.3.tgz +0 -0
- package/package.json +23 -10
- package/patches/notion-exporter+0.8.1.patch +32 -0
- package/src/commands/html.ts +135 -0
- package/src/commands/init.ts +55 -0
- package/src/commands/md.ts +0 -4
- package/src/commands/pdf.ts +150 -0
- package/src/config.ts +105 -13
- package/src/core/exporter.ts +159 -2
- package/src/index.ts +52 -3
- package/src/repl/commands.ts +535 -0
- package/src/repl/index.ts +87 -0
- package/src/repl/prompts.ts +123 -0
- package/src/utils/logger.ts +5 -5
- package/test/test-export-types.ts +54 -0
- package/test/test-markdown.ts +45 -0
- package/test/test-pdf.ts +44 -0
- package/test-output/drf-serializer/drf-serializer.pdf +0 -0
- package/test-output/drf-serializer/images/Untitled-1.png +0 -0
- package/test-output/drf-serializer/images/Untitled.png +0 -0
- package/README.en.md +0 -200
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { input, confirm } from '@inquirer/prompts';
|
|
2
|
+
import * as logger from '../utils/logger.js';
|
|
3
|
+
import { mdCommand } from '../commands/md.js';
|
|
4
|
+
import { htmlCommand } from '../commands/html.js';
|
|
5
|
+
import { pdfCommand } from '../commands/pdf.js';
|
|
6
|
+
import { validateConfig } from '../config.js';
|
|
7
|
+
import {
|
|
8
|
+
loadConfig,
|
|
9
|
+
saveConfig,
|
|
10
|
+
promptInitConfig,
|
|
11
|
+
promptEditConfig,
|
|
12
|
+
} from './prompts.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle /init command - Initialize configuration
|
|
16
|
+
*/
|
|
17
|
+
export async function handleInit(): Promise<void> {
|
|
18
|
+
const existing = loadConfig();
|
|
19
|
+
const isFirstTime = !existing || (!existing.TOKEN_V2 && !existing.FILE_TOKEN);
|
|
20
|
+
|
|
21
|
+
// If config already exists, ask for confirmation
|
|
22
|
+
if (!isFirstTime) {
|
|
23
|
+
logger.warn('Configuration already exists.');
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const overwrite = await confirm({
|
|
27
|
+
message: 'Do you want to overwrite the existing configuration?',
|
|
28
|
+
default: false,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!overwrite) {
|
|
32
|
+
logger.info('Configuration unchanged. Use /config to view or edit your settings.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error instanceof Error && error.message === 'User force closed the prompt') {
|
|
37
|
+
logger.warn('\nCancelled.');
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Show detailed instructions for token setup
|
|
44
|
+
logger.info('\nš How to find your Notion tokens\n');
|
|
45
|
+
logger.info('1. Log in to https://notion.so in your browser');
|
|
46
|
+
logger.info('2. Open browser developer tools (press F12)');
|
|
47
|
+
logger.info('3. Go to the "Application" tab');
|
|
48
|
+
logger.info('4. Find "Cookies" section and select https://www.notion.so');
|
|
49
|
+
logger.info('5. Copy the value of "token_v2" cookie ā TOKEN_V2');
|
|
50
|
+
logger.info('6. Copy the value of "file_token" cookie ā FILE_TOKEN\n');
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const config = await promptInitConfig();
|
|
54
|
+
saveConfig(config);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof Error && error.message === 'User force closed the prompt') {
|
|
57
|
+
logger.warn('\nConfiguration cancelled.');
|
|
58
|
+
} else {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Handle /config command - View and edit configuration
|
|
66
|
+
*/
|
|
67
|
+
export async function handleConfig(): Promise<void> {
|
|
68
|
+
const existing = loadConfig();
|
|
69
|
+
|
|
70
|
+
if (!existing || (!existing.TOKEN_V2 && !existing.FILE_TOKEN)) {
|
|
71
|
+
logger.warn('No configuration found.');
|
|
72
|
+
logger.info('Run /init to create initial configuration.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logger.info('Current configuration:');
|
|
77
|
+
logger.info(`TOKEN_V2: ${existing.TOKEN_V2 ? '***' + existing.TOKEN_V2.slice(-8) : '(not set)'}`);
|
|
78
|
+
logger.info(`FILE_TOKEN: ${existing.FILE_TOKEN ? '***' + existing.FILE_TOKEN.slice(-8) : '(not set)'}\n`);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const edit = await input({
|
|
82
|
+
message: 'Edit configuration? (y/n)',
|
|
83
|
+
default: 'n',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (edit.toLowerCase() === 'y' || edit.toLowerCase() === 'yes') {
|
|
87
|
+
const newConfig = await promptEditConfig(existing);
|
|
88
|
+
saveConfig(newConfig);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof Error && error.message === 'User force closed the prompt') {
|
|
92
|
+
logger.warn('\nEdit cancelled.');
|
|
93
|
+
} else {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Handle /md command - Convert Notion page to markdown
|
|
101
|
+
*/
|
|
102
|
+
export async function handleMd(args: string[]): Promise<void> {
|
|
103
|
+
let url = '';
|
|
104
|
+
const options: any = {
|
|
105
|
+
output: './nconv-output',
|
|
106
|
+
imageDir: 'images',
|
|
107
|
+
verbose: false,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Interactive mode (TUI) - step by step prompts
|
|
111
|
+
if (args.length === 0) {
|
|
112
|
+
try {
|
|
113
|
+
// Step 1: URL
|
|
114
|
+
url = await input({
|
|
115
|
+
message: 'Notion URL',
|
|
116
|
+
validate: (value) => {
|
|
117
|
+
if (!value.trim()) return 'URL is required';
|
|
118
|
+
if (!value.includes('notion.so') && !value.includes('notion.site')) {
|
|
119
|
+
return 'Please enter a valid Notion URL';
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Step 2: Output directory
|
|
126
|
+
const outputDir = await input({
|
|
127
|
+
message: 'Output directory [default: ./nconv-output]',
|
|
128
|
+
default: './nconv-output',
|
|
129
|
+
});
|
|
130
|
+
options.output = outputDir;
|
|
131
|
+
|
|
132
|
+
// Step 3: Filename (optional, auto-generated if empty)
|
|
133
|
+
const filename = await input({
|
|
134
|
+
message: 'Filename [leave empty for auto-generated]',
|
|
135
|
+
default: '',
|
|
136
|
+
});
|
|
137
|
+
if (filename.trim()) {
|
|
138
|
+
options.filename = filename;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Step 4: Verbose logging
|
|
142
|
+
options.verbose = await confirm({
|
|
143
|
+
message: 'Enable verbose logging?',
|
|
144
|
+
default: false,
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof Error && error.message === 'User force closed the prompt') {
|
|
148
|
+
logger.warn('\nConversion cancelled.');
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
// CLI mode - parse arguments
|
|
154
|
+
url = args[0];
|
|
155
|
+
const additionalArgs = args.slice(1);
|
|
156
|
+
|
|
157
|
+
// Parse options
|
|
158
|
+
for (let i = 0; i < additionalArgs.length; i++) {
|
|
159
|
+
const arg = additionalArgs[i];
|
|
160
|
+
if ((arg === '-o' || arg === '--output') && i + 1 < additionalArgs.length) {
|
|
161
|
+
options.output = additionalArgs[++i];
|
|
162
|
+
} else if ((arg === '-i' || arg === '--image-dir') && i + 1 < additionalArgs.length) {
|
|
163
|
+
options.imageDir = additionalArgs[++i];
|
|
164
|
+
} else if ((arg === '-f' || arg === '--filename') && i + 1 < additionalArgs.length) {
|
|
165
|
+
options.filename = additionalArgs[++i];
|
|
166
|
+
} else if (arg === '-v' || arg === '--verbose') {
|
|
167
|
+
options.verbose = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check if config is valid
|
|
173
|
+
const configCheck = validateConfig();
|
|
174
|
+
if (!configCheck.valid) {
|
|
175
|
+
logger.error('Cannot convert Notion page:');
|
|
176
|
+
logger.error(configCheck.message || 'Configuration is invalid.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await mdCommand(url, options);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle /html command - Convert Notion page to HTML
|
|
185
|
+
*/
|
|
186
|
+
export async function handleHtml(args: string[]): Promise<void> {
|
|
187
|
+
let url = '';
|
|
188
|
+
const options: any = {
|
|
189
|
+
output: './nconv-output',
|
|
190
|
+
imageDir: 'images',
|
|
191
|
+
verbose: false,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Interactive mode (TUI) - step by step prompts
|
|
195
|
+
if (args.length === 0) {
|
|
196
|
+
try {
|
|
197
|
+
// Step 1: URL
|
|
198
|
+
url = await input({
|
|
199
|
+
message: 'Notion URL',
|
|
200
|
+
validate: (value) => {
|
|
201
|
+
if (!value.trim()) return 'URL is required';
|
|
202
|
+
if (!value.includes('notion.so') && !value.includes('notion.site')) {
|
|
203
|
+
return 'Please enter a valid Notion URL';
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Step 2: Output directory
|
|
210
|
+
const outputDir = await input({
|
|
211
|
+
message: 'Output directory [default: ./nconv-output]',
|
|
212
|
+
default: './nconv-output',
|
|
213
|
+
});
|
|
214
|
+
options.output = outputDir;
|
|
215
|
+
|
|
216
|
+
// Step 3: Filename (optional, auto-generated if empty)
|
|
217
|
+
const filename = await input({
|
|
218
|
+
message: 'Filename [leave empty for auto-generated]',
|
|
219
|
+
default: '',
|
|
220
|
+
});
|
|
221
|
+
if (filename.trim()) {
|
|
222
|
+
options.filename = filename;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Step 4: Verbose logging
|
|
226
|
+
options.verbose = await confirm({
|
|
227
|
+
message: 'Enable verbose logging?',
|
|
228
|
+
default: false,
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error instanceof Error && error.message === 'User force closed the prompt') {
|
|
232
|
+
logger.warn('\nConversion cancelled.');
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
// CLI mode - parse arguments
|
|
238
|
+
url = args[0];
|
|
239
|
+
const additionalArgs = args.slice(1);
|
|
240
|
+
|
|
241
|
+
// Parse options
|
|
242
|
+
for (let i = 0; i < additionalArgs.length; i++) {
|
|
243
|
+
const arg = additionalArgs[i];
|
|
244
|
+
if ((arg === '-o' || arg === '--output') && i + 1 < additionalArgs.length) {
|
|
245
|
+
options.output = additionalArgs[++i];
|
|
246
|
+
} else if ((arg === '-i' || arg === '--image-dir') && i + 1 < additionalArgs.length) {
|
|
247
|
+
options.imageDir = additionalArgs[++i];
|
|
248
|
+
} else if ((arg === '-f' || arg === '--filename') && i + 1 < additionalArgs.length) {
|
|
249
|
+
options.filename = additionalArgs[++i];
|
|
250
|
+
} else if (arg === '-v' || arg === '--verbose') {
|
|
251
|
+
options.verbose = true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check if config is valid
|
|
257
|
+
const configCheck = validateConfig();
|
|
258
|
+
if (!configCheck.valid) {
|
|
259
|
+
logger.error('Cannot convert Notion page:');
|
|
260
|
+
logger.error(configCheck.message || 'Configuration is invalid.');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await htmlCommand(url, options);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Handle /pdf command - Convert Notion page to PDF
|
|
269
|
+
*/
|
|
270
|
+
export async function handlePdf(args: string[]): Promise<void> {
|
|
271
|
+
let url = '';
|
|
272
|
+
const options: any = {
|
|
273
|
+
output: './nconv-output',
|
|
274
|
+
imageDir: 'images',
|
|
275
|
+
verbose: false,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Interactive mode (TUI) - step by step prompts
|
|
279
|
+
if (args.length === 0) {
|
|
280
|
+
try {
|
|
281
|
+
// Step 1: URL
|
|
282
|
+
url = await input({
|
|
283
|
+
message: 'Notion URL',
|
|
284
|
+
validate: (value) => {
|
|
285
|
+
if (!value.trim()) return 'URL is required';
|
|
286
|
+
if (!value.includes('notion.so') && !value.includes('notion.site')) {
|
|
287
|
+
return 'Please enter a valid Notion URL';
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Step 2: Output directory
|
|
294
|
+
const outputDir = await input({
|
|
295
|
+
message: 'Output directory [default: ./nconv-output]',
|
|
296
|
+
default: './nconv-output',
|
|
297
|
+
});
|
|
298
|
+
options.output = outputDir;
|
|
299
|
+
|
|
300
|
+
// Step 3: Filename (optional, auto-generated if empty)
|
|
301
|
+
const filename = await input({
|
|
302
|
+
message: 'Filename [leave empty for auto-generated]',
|
|
303
|
+
default: '',
|
|
304
|
+
});
|
|
305
|
+
if (filename.trim()) {
|
|
306
|
+
options.filename = filename;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Step 4: Verbose logging
|
|
310
|
+
options.verbose = await confirm({
|
|
311
|
+
message: 'Enable verbose logging?',
|
|
312
|
+
default: false,
|
|
313
|
+
});
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (error instanceof Error && error.message === 'User force closed the prompt') {
|
|
316
|
+
logger.warn('\nConversion cancelled.');
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
// CLI mode - parse arguments
|
|
322
|
+
url = args[0];
|
|
323
|
+
const additionalArgs = args.slice(1);
|
|
324
|
+
|
|
325
|
+
// Parse options
|
|
326
|
+
for (let i = 0; i < additionalArgs.length; i++) {
|
|
327
|
+
const arg = additionalArgs[i];
|
|
328
|
+
if ((arg === '-o' || arg === '--output') && i + 1 < additionalArgs.length) {
|
|
329
|
+
options.output = additionalArgs[++i];
|
|
330
|
+
} else if ((arg === '-f' || arg === '--filename') && i + 1 < additionalArgs.length) {
|
|
331
|
+
options.filename = additionalArgs[++i];
|
|
332
|
+
} else if (arg === '-v' || arg === '--verbose') {
|
|
333
|
+
options.verbose = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check if config is valid
|
|
339
|
+
const configCheck = validateConfig();
|
|
340
|
+
if (!configCheck.valid) {
|
|
341
|
+
logger.error('Cannot convert Notion page:');
|
|
342
|
+
logger.error(configCheck.message || 'Configuration is invalid.');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
await pdfCommand(url, options);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Available commands list
|
|
351
|
+
*/
|
|
352
|
+
const AVAILABLE_COMMANDS = [
|
|
353
|
+
{
|
|
354
|
+
name: 'init',
|
|
355
|
+
description: 'Initialize configuration (set Notion tokens)',
|
|
356
|
+
examples: ['/init'],
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'config',
|
|
360
|
+
description: 'View and edit current configuration',
|
|
361
|
+
examples: ['/config'],
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: 'md',
|
|
365
|
+
description: 'Convert Notion page to markdown',
|
|
366
|
+
examples: [
|
|
367
|
+
'/md https://notion.so/page-id',
|
|
368
|
+
'/md https://notion.so/page-id -o ./blog',
|
|
369
|
+
'/md https://notion.so/page-id -o ./blog -f my-post -v',
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'html',
|
|
374
|
+
description: 'Convert Notion page to HTML',
|
|
375
|
+
examples: [
|
|
376
|
+
'/html https://notion.so/page-id',
|
|
377
|
+
'/html https://notion.so/page-id -o ./blog',
|
|
378
|
+
'/html https://notion.so/page-id -o ./blog -f my-post -v',
|
|
379
|
+
],
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'pdf',
|
|
383
|
+
description: 'Convert Notion page to PDF (renders actual page)',
|
|
384
|
+
examples: [
|
|
385
|
+
'/pdf https://notion.so/page-id',
|
|
386
|
+
'/pdf https://notion.so/page-id -o ./blog',
|
|
387
|
+
'/pdf https://notion.so/page-id -o ./blog -f my-post -v',
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'help',
|
|
392
|
+
description: 'Show this help message',
|
|
393
|
+
examples: ['/help'],
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: 'exit',
|
|
397
|
+
description: 'Exit the REPL',
|
|
398
|
+
examples: ['/exit'],
|
|
399
|
+
},
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get command suggestions based on input
|
|
404
|
+
*/
|
|
405
|
+
export function getCommandSuggestions(input: string): string[] {
|
|
406
|
+
const cleanInput = input.toLowerCase().replace(/^\//, '');
|
|
407
|
+
return AVAILABLE_COMMANDS
|
|
408
|
+
.filter((cmd) => cmd.name.startsWith(cleanInput))
|
|
409
|
+
.map((cmd) => `/${cmd.name}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Get command choices for autocomplete
|
|
414
|
+
*/
|
|
415
|
+
export function getCommandChoices() {
|
|
416
|
+
return AVAILABLE_COMMANDS.map((cmd) => ({
|
|
417
|
+
title: `/${cmd.name}`,
|
|
418
|
+
value: `/${cmd.name}`,
|
|
419
|
+
description: cmd.description,
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Find similar commands using simple string distance
|
|
425
|
+
*/
|
|
426
|
+
function findSimilarCommand(input: string): string | null {
|
|
427
|
+
const cleanInput = input.toLowerCase();
|
|
428
|
+
for (const cmd of AVAILABLE_COMMANDS) {
|
|
429
|
+
if (cmd.name.includes(cleanInput) || cleanInput.includes(cmd.name)) {
|
|
430
|
+
return cmd.name;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Handle /help command - Show help information
|
|
438
|
+
*/
|
|
439
|
+
export function handleHelp(): void {
|
|
440
|
+
logger.info('Available commands:\n');
|
|
441
|
+
|
|
442
|
+
AVAILABLE_COMMANDS.forEach((cmd) => {
|
|
443
|
+
logger.info(` /${cmd.name.padEnd(20)} ${cmd.description}`);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
console.log('');
|
|
447
|
+
logger.info('Conversion options (for /md, /html, and /pdf):');
|
|
448
|
+
logger.info(' -o, --output <dir> Output directory (default: ./nconv-output)');
|
|
449
|
+
logger.info(' -i, --image-dir <dir> Image folder name (default: images) [md/html only]');
|
|
450
|
+
logger.info(' -f, --filename <name> Output filename');
|
|
451
|
+
logger.info(' -v, --verbose Enable verbose logging\n');
|
|
452
|
+
|
|
453
|
+
logger.info('Examples:');
|
|
454
|
+
AVAILABLE_COMMANDS.forEach((cmd) => {
|
|
455
|
+
cmd.examples.forEach((example) => {
|
|
456
|
+
logger.info(` ${example}`);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
console.log('');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Parse and execute a slash command
|
|
464
|
+
*/
|
|
465
|
+
export async function executeCommand(input: string): Promise<boolean> {
|
|
466
|
+
const trimmed = input.trim();
|
|
467
|
+
|
|
468
|
+
if (!trimmed) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!trimmed.startsWith('/')) {
|
|
473
|
+
logger.error('Commands must start with /');
|
|
474
|
+
logger.info('Type /help for available commands');
|
|
475
|
+
logger.info('Example: /init, /md <url>, /config\n');
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
480
|
+
const command = parts[0].toLowerCase();
|
|
481
|
+
const args = parts.slice(1);
|
|
482
|
+
|
|
483
|
+
switch (command) {
|
|
484
|
+
case 'init':
|
|
485
|
+
await handleInit();
|
|
486
|
+
break;
|
|
487
|
+
|
|
488
|
+
case 'config':
|
|
489
|
+
await handleConfig();
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
case 'md':
|
|
493
|
+
await handleMd(args);
|
|
494
|
+
break;
|
|
495
|
+
|
|
496
|
+
case 'html':
|
|
497
|
+
await handleHtml(args);
|
|
498
|
+
break;
|
|
499
|
+
|
|
500
|
+
case 'pdf':
|
|
501
|
+
await handlePdf(args);
|
|
502
|
+
break;
|
|
503
|
+
|
|
504
|
+
case 'help':
|
|
505
|
+
case 'h':
|
|
506
|
+
handleHelp();
|
|
507
|
+
break;
|
|
508
|
+
|
|
509
|
+
case 'exit':
|
|
510
|
+
case 'quit':
|
|
511
|
+
case 'q':
|
|
512
|
+
return true;
|
|
513
|
+
|
|
514
|
+
default:
|
|
515
|
+
logger.error(`Unknown command: /${command}`);
|
|
516
|
+
|
|
517
|
+
// Try to suggest a similar command
|
|
518
|
+
const similar = findSimilarCommand(command);
|
|
519
|
+
if (similar) {
|
|
520
|
+
logger.info(`Did you mean /${similar}?`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
logger.info('Type /help to see all available commands\n');
|
|
524
|
+
|
|
525
|
+
// Show available commands
|
|
526
|
+
const suggestions = getCommandSuggestions(command);
|
|
527
|
+
if (suggestions.length > 0 && suggestions.length < 5) {
|
|
528
|
+
logger.info('Available commands:');
|
|
529
|
+
suggestions.forEach((cmd) => logger.info(` ${cmd}`));
|
|
530
|
+
console.log('');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as logger from '../utils/logger.js';
|
|
4
|
+
import { executeCommand, getCommandChoices } from './commands.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Display a centered banner with dynamic box sizing
|
|
8
|
+
*/
|
|
9
|
+
function showBanner(): void {
|
|
10
|
+
const title = 'NCONV CLI (Notion Converter CLI)';
|
|
11
|
+
const padding = 2;
|
|
12
|
+
const totalWidth = title.length + (padding * 2);
|
|
13
|
+
|
|
14
|
+
const topBorder = 'ā' + 'ā'.repeat(totalWidth) + 'ā';
|
|
15
|
+
const bottomBorder = 'ā' + 'ā'.repeat(totalWidth) + 'ā';
|
|
16
|
+
|
|
17
|
+
console.log(chalk.cyan('\n' + topBorder));
|
|
18
|
+
console.log(chalk.cyan('ā') + chalk.bold(' '.repeat(padding) + title + ' '.repeat(padding)) + chalk.cyan('ā'));
|
|
19
|
+
console.log(chalk.cyan(bottomBorder + '\n'));
|
|
20
|
+
|
|
21
|
+
logger.info('Welcome to nconv interactive mode!');
|
|
22
|
+
logger.info('Type /help to see available commands');
|
|
23
|
+
logger.info('Type /exit to quit\n');
|
|
24
|
+
|
|
25
|
+
// Show quick examples
|
|
26
|
+
console.log(chalk.dim('Quick examples:'));
|
|
27
|
+
console.log(chalk.dim(' /init - Set up Notion tokens'));
|
|
28
|
+
console.log(chalk.dim(' /md <url> - Convert Notion page'));
|
|
29
|
+
console.log(chalk.dim(' /md <url> -o ./blog -f my-post - Convert with options\n'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start the REPL loop
|
|
34
|
+
*/
|
|
35
|
+
export async function startRepl(): Promise<void> {
|
|
36
|
+
showBanner();
|
|
37
|
+
|
|
38
|
+
let shouldExit = false;
|
|
39
|
+
|
|
40
|
+
while (!shouldExit) {
|
|
41
|
+
try {
|
|
42
|
+
const response = await prompts({
|
|
43
|
+
type: 'autocomplete',
|
|
44
|
+
name: 'command',
|
|
45
|
+
message: chalk.cyan('nconv'),
|
|
46
|
+
choices: getCommandChoices(),
|
|
47
|
+
suggest: async (input: string, choices: any[]) => {
|
|
48
|
+
// If input doesn't start with /, add it for search
|
|
49
|
+
const searchInput = input.startsWith('/') ? input : `/${input}`;
|
|
50
|
+
|
|
51
|
+
// Filter choices based on input
|
|
52
|
+
const filtered = choices.filter((choice: any) =>
|
|
53
|
+
choice.title.toLowerCase().startsWith(searchInput.toLowerCase())
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// If user typed a full command or URL, allow it
|
|
57
|
+
if (filtered.length === 0 && input.trim()) {
|
|
58
|
+
return Promise.resolve([{ title: input, value: input }]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Promise.resolve(filtered);
|
|
62
|
+
},
|
|
63
|
+
limit: 5,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (response.command === undefined) {
|
|
67
|
+
// User cancelled (Ctrl+C)
|
|
68
|
+
console.log('\n');
|
|
69
|
+
logger.info('Goodbye! š');
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
shouldExit = await executeCommand(response.command);
|
|
74
|
+
|
|
75
|
+
if (!shouldExit) {
|
|
76
|
+
console.log(); // Empty line for readability
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof Error) {
|
|
80
|
+
logger.error(`Error: ${error.message}`);
|
|
81
|
+
console.log(); // Empty line for readability
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logger.info('Exiting nconv...');
|
|
87
|
+
}
|