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
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/core/exporter.ts","../src/utils/file.ts","../src/utils/logger.ts","../src/commands/md.ts","../src/commands/debug.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport { mdCommand } from './commands/md.js';\nimport { debugCommand } from './commands/debug.js';\n\nconst program = new Command();\n\nprogram\n .name('nconv')\n .description('CLI tool for converting Notion pages to blog-ready markdown')\n .version('1.0.0');\n\nprogram\n .command('md <url>')\n .description('Convert a Notion page to markdown')\n .option('-o, --output <dir>', 'Output directory', './output')\n .option('-i, --image-dir <dir>', 'Image folder name (relative to output)', 'images')\n .option('-f, --filename <name>', '출력 파일명 (확장자 제외 또는 포함)')\n .option('-v, --verbose', 'Enable verbose logging', false)\n .action(async (url: string, options) => {\n await mdCommand(url, {\n output: options.output,\n imageDir: options.imageDir,\n filename: options.filename,\n verbose: options.verbose,\n });\n });\n\n// 개발 환경에서만 debug 명령어를 활성화\nif (process.env.NODE_ENV !== 'production') {\n program\n .command('debug <url>')\n .description('Debug: Output raw markdown and image URLs')\n .option('-o, --output <dir>', '출력 디렉토리', './output')\n .option('-i, --image-dir <dir>', 'Image folder name', 'images')\n .option('-v, --verbose', 'Enable verbose logging', false)\n .action(async (url: string, options) => {\n await debugCommand(url, {\n output: options.output,\n imageDir: options.imageDir,\n filename: '',\n verbose: options.verbose,\n });\n });\n}\n\nprogram.parse();\n","import { config as dotenvConfig } from 'dotenv';\nimport { existsSync } from 'fs';\nimport { resolve } from 'path';\n\n// 환경 변수 로드\ndotenvConfig();\n\nexport interface NotionConfig {\n tokenV2: string;\n fileToken: string;\n}\n\nexport interface ConverterOptions {\n output: string;\n imageDir: string;\n filename?: string;\n verbose: boolean;\n}\n\nexport interface FullConfig extends NotionConfig, ConverterOptions {}\n\n/**\n * Notion 인증 설정 가져오기\n */\nexport function getNotionConfig(): NotionConfig {\n const tokenV2 = process.env.TOKEN_V2 || '';\n const fileToken = process.env.FILE_TOKEN || '';\n\n if (!tokenV2 || !fileToken) {\n throw new Error(\n 'Notion 토큰이 설정되지 않았습니다.\\n' +\n '.env 파일에 TOKEN_V2와 FILE_TOKEN을 설정해주세요.\\n' +\n '자세한 내용은 .env.example을 참고하세요.'\n );\n }\n\n return { tokenV2, fileToken };\n}\n\n/**\n * 전체 설정 생성\n */\nexport function createConfig(options: ConverterOptions): FullConfig {\n const notionConfig = getNotionConfig();\n\n // 출력 디렉토리 절대 경로로 변환\n const outputDir = resolve(process.cwd(), options.output);\n\n return {\n ...notionConfig,\n ...options,\n output: outputDir,\n };\n}\n\n/**\n * 디렉토리 존재 여부 확인\n */\nexport function ensureDirectoryExists(dir: string): boolean {\n return existsSync(dir);\n}\n","import { NotionExporter } from 'notion-exporter';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport type { NotionConfig } from '../config.js';\n\nexport interface ExportResult {\n markdown: string;\n imageFiles: Array<{ filename: string; sourcePath: string }>;\n}\n\n/**\n * Notion 페이지를 마크다운으로 내보내기\n */\nexport class NotionMarkdownExporter {\n private exporter: NotionExporter;\n\n constructor(config: NotionConfig) {\n this.exporter = new NotionExporter(config.tokenV2, config.fileToken);\n }\n\n /**\n * Notion URL에서 마크다운과 이미지 파일 가져오기\n */\n async exportWithImages(notionUrl: string, tempDir: string): Promise<ExportResult> {\n try {\n // 임시 디렉토리 생성\n await fs.mkdir(tempDir, { recursive: true });\n\n // getMdFiles로 마크다운과 이미지를 함께 다운로드\n await this.exporter.getMdFiles(notionUrl, tempDir);\n\n // 다운로드된 파일 목록 가져오기\n const files = await fs.readdir(tempDir, { withFileTypes: true });\n\n // 마크다운 파일 찾기\n const mdFile = files.find(f => f.isFile() && f.name.endsWith('.md'));\n if (!mdFile) {\n throw new Error('Markdown file not found.');\n }\n\n // 마크다운 내용 읽기\n const mdPath = path.join(tempDir, mdFile.name);\n const markdown = await fs.readFile(mdPath, 'utf-8');\n\n // 이미지 파일 목록 (마크다운이 아닌 파일들)\n const imageFiles = files\n .filter(f => f.isFile() && !f.name.endsWith('.md'))\n .map(f => ({\n filename: f.name,\n sourcePath: path.join(tempDir, f.name),\n }));\n\n // 디렉토리 내 폴더도 확인 (Notion이 폴더 구조로 저장할 수 있음)\n const dirs = files.filter(f => f.isDirectory());\n for (const dir of dirs) {\n const subFiles = await fs.readdir(path.join(tempDir, dir.name), { withFileTypes: true });\n for (const subFile of subFiles) {\n if (subFile.isFile() && !subFile.name.endsWith('.md')) {\n imageFiles.push({\n filename: path.join(dir.name, subFile.name),\n sourcePath: path.join(tempDir, dir.name, subFile.name),\n });\n }\n }\n }\n\n return { markdown, imageFiles };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to fetch Notion page: ${error.message}`);\n }\n throw new Error('Failed to fetch Notion page.');\n }\n }\n}\n","import { promises as fs } from 'fs';\nimport path from 'path';\nimport slugify from 'slugify';\nimport { v4 as uuidv4 } from 'uuid';\n\n/**\n * Notion URL에서 페이지 제목 추출 (간단한 버전)\n */\nexport function extractTitleFromUrl(notionUrl: string): string | null {\n try {\n const url = new URL(notionUrl);\n const pathname = url.pathname;\n\n // /Page-Title-abc123 형식에서 제목 부분 추출\n const match = pathname.match(/\\/([^/]+)-[a-f0-9]{32}$/i);\n if (match) {\n const title = match[1].replace(/-/g, ' ');\n return title;\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * 안전한 파일명 생성\n */\nexport function generateSafeFilename(title: string | null, extension: string = 'md'): string {\n if (title) {\n const slug = slugify(title, {\n lower: true,\n strict: true,\n locale: 'ko',\n });\n\n if (slug.length > 0) {\n return extension ? `${slug}.${extension}` : slug;\n }\n }\n\n // 제목이 없거나 slugify 실패 시 UUID 사용\n const hash = uuidv4().slice(0, 8);\n return extension ? `notion-export-${hash}.${extension}` : `notion-export-${hash}`;\n}\n\n/**\n * 마크다운 파일 저장\n */\nexport async function saveMarkdownFile(\n outputDir: string,\n filename: string,\n content: string\n): Promise<string> {\n await fs.mkdir(outputDir, { recursive: true });\n\n const filePath = path.join(outputDir, filename);\n await fs.writeFile(filePath, content, 'utf-8');\n\n return filePath;\n}\n\n/**\n * 파일 경로를 상대 경로로 변환\n */\nexport function toRelativePath(from: string, to: string): string {\n return path.relative(from, to);\n}\n","import chalk from 'chalk';\nimport ora, { Ora } from 'ora';\n\n/**\n * 성공 메시지 출력\n */\nexport function success(message: string): void {\n console.log(chalk.green('✓'), message);\n}\n\n/**\n * 에러 메시지 출력\n */\nexport function error(message: string): void {\n console.error(chalk.red('✗'), message);\n}\n\n/**\n * 정보 메시지 출력\n */\nexport function info(message: string): void {\n console.log(chalk.blue('ℹ'), message);\n}\n\n/**\n * 경고 메시지 출력\n */\nexport function warn(message: string): void {\n console.log(chalk.yellow('⚠'), message);\n}\n\n/**\n * 스피너 생성\n */\nexport function spinner(text: string): Ora {\n return ora(text).start();\n}\n","import { ConverterOptions, createConfig } from '../config.js';\nimport { NotionMarkdownExporter } from '../core/exporter.js';\nimport { generateSafeFilename, extractTitleFromUrl, saveMarkdownFile } from '../utils/file.js';\nimport * as logger from '../utils/logger.js';\nimport path from 'path';\nimport { promises as fs } from 'fs';\nimport os from 'os';\n\n/**\n * md 명령어 핸들러\n */\nexport async function mdCommand(notionUrl: string, options: ConverterOptions) {\n const tempDir = path.join(os.tmpdir(), `nconv-cli-${Date.now()}`);\n\n try {\n // 설정 생성\n const config = createConfig(options);\n\n if (config.verbose) {\n logger.info('Configuration loaded successfully');\n console.log(` Output directory: ${config.output}\\n`);\n }\n\n // 1. Notion에서 마크다운과 이미지 가져오기\n const spinner = logger.spinner('Fetching Notion page...');\n\n const exporter = new NotionMarkdownExporter({\n tokenV2: config.tokenV2,\n fileToken: config.fileToken,\n });\n\n let result;\n try {\n result = await exporter.exportWithImages(notionUrl, tempDir);\n spinner.succeed(`Notion page fetched (${result.imageFiles.length} images)`);\n } catch (error) {\n spinner.fail('Failed to fetch Notion page');\n throw error;\n }\n\n // 2. 파일명/폴더명 생성\n let baseFilename: string;\n if (config.filename) {\n baseFilename = config.filename.replace(/\\.md$/, '');\n } else {\n const title = extractTitleFromUrl(notionUrl);\n baseFilename = generateSafeFilename(title, ''); // 확장자 없이 생성\n }\n\n // 3. 제목별 폴더 생성 (output/제목/)\n const pageDir = path.join(config.output, baseFilename);\n await fs.mkdir(pageDir, { recursive: true });\n\n if (config.verbose) {\n console.log(`📁 출력 폴더: ${path.relative(process.cwd(), pageDir)}\\n`);\n }\n\n // 4. 이미지 폴더 생성 및 이미지 파일 이동\n const imageOutputDir = path.join(pageDir, config.imageDir);\n await fs.mkdir(imageOutputDir, { recursive: true });\n\n if (config.verbose && result.imageFiles.length > 0) {\n console.log(`Processing image files...\\n`);\n }\n\n let processedMarkdown = result.markdown;\n for (const imageFile of result.imageFiles) {\n try {\n // 파일명에서 공백을 하이픈으로 변경 (마크다운 호환성)\n const originalFileName = path.basename(imageFile.filename);\n const safeFileName = originalFileName.replace(/\\s+/g, '-');\n\n // 이미지 파일 복사\n const targetPath = path.join(imageOutputDir, safeFileName);\n await fs.copyFile(imageFile.sourcePath, targetPath);\n\n if (config.verbose) {\n console.log(`✓ ${safeFileName}`);\n }\n\n // 마크다운 내 경로를 상대경로로 변경\n const originalPath = imageFile.filename;\n const relativePath = `./${config.imageDir}/${safeFileName}`;\n\n // Notion이 URL 인코딩하는 방식: 각 경로 부분을 개별적으로 인코딩\n const pathParts = originalPath.split('/');\n const encodedPath = pathParts.map(part => encodeURIComponent(part)).join('/');\n\n // 정규식 특수문자 이스케이프 함수\n const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n // 모든 가능한 형태의 경로를 교체\n processedMarkdown = processedMarkdown\n .replace(new RegExp(`\\\\(${escapeRegex(originalPath)}\\\\)`, 'g'), `(${relativePath})`)\n .replace(new RegExp(`\\\\(${escapeRegex(encodedPath)}\\\\)`, 'g'), `(${relativePath})`);\n\n } catch (error) {\n if (config.verbose) {\n const errorMsg = error instanceof Error ? error.message : '알 수 없는 오류';\n console.error(`✗ ${imageFile.filename}: ${errorMsg}`);\n }\n }\n }\n\n // 5. 마크다운 파일 저장 (제목 폴더 안에)\n const filename = `${baseFilename}.md`;\n const filePath = await saveMarkdownFile(pageDir, filename, processedMarkdown);\n\n // 6. 결과 출력\n console.log('');\n logger.success('Conversion complete!');\n console.log('');\n console.log(`📁 Folder: ${path.relative(process.cwd(), pageDir)}`);\n console.log(`📄 Markdown: ${filename}`);\n\n if (result.imageFiles.length > 0) {\n console.log(`🖼️ Images: ${config.imageDir}/ (${result.imageFiles.length} files)`);\n }\n\n console.log('');\n\n } catch (error) {\n if (error instanceof Error) {\n logger.error(error.message);\n } else {\n logger.error('An unknown error occurred.');\n }\n process.exit(1);\n } finally {\n // 임시 디렉토리 정리\n try {\n await fs.rm(tempDir, { recursive: true, force: true });\n } catch {\n // 무시\n }\n }\n}\n","import { ConverterOptions, createConfig } from '../config.js';\nimport { NotionMarkdownExporter } from '../core/exporter.js';\nimport * as fs from 'fs/promises';\nimport * as os from 'os';\nimport * as path from 'path';\n\n/**\n * Debugging command - output raw markdown\n */\nexport async function debugCommand(notionUrl: string, options: ConverterOptions) {\n let tempDir: string | undefined;\n try {\n const config = createConfig(options);\n\n const exporter = new NotionMarkdownExporter({\n tokenV2: config.tokenV2,\n fileToken: config.fileToken,\n });\n\n tempDir = path.join(os.tmpdir(), `notion-debug-${Date.now()}`); // Create temporary directory\n\n console.log('Fetching Notion page...\\n');\n const { markdown, imageFiles } = await exporter.exportWithImages(notionUrl, tempDir);\n\n console.log('=== Raw Markdown ===\\n');\n console.log(markdown.slice(0, 3000));\n console.log('\\n... (truncated) ...\\n');\n\n console.log(`\\n=== Found Images (${imageFiles.length}) ===\\n`);\n imageFiles.forEach((file, i) => {\n console.log(`${i + 1}. ${file.filename} (원본 경로: ${file.sourcePath})`);\n });\n\n } catch (error) {\n console.error('Error:', error);\n } finally {\n if (tempDir) {\n // Clean up temporary directory\n await fs.rm(tempDir, { recursive: true, force: true }).catch(err => {\n console.warn(`Error cleaning up temporary directory: ${err.message}`);\n });\n }\n }\n}"],"mappings":"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","names":["error","fs","path","path","fs","spinner","error","fs","os","path","error"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/utils/logger.ts","../src/core/exporter.ts","../src/utils/file.ts","../src/commands/md.ts","../src/commands/html.ts","../src/commands/pdf.ts","../src/commands/debug.ts","../src/commands/init.ts","../src/repl/index.ts","../src/repl/commands.ts","../src/repl/prompts.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport { mdCommand } from './commands/md.js';\nimport { htmlCommand } from './commands/html.js';\nimport { pdfCommand } from './commands/pdf.js';\nimport { debugCommand } from './commands/debug.js';\nimport { handler as initHandler } from './commands/init.js';\nimport { startRepl } from './repl/index.js';\n\nconst program = new Command();\n\nprogram\n .name('nconv')\n .description('CLI tool for converting Notion pages to blog-ready markdown')\n .version('1.0.0');\n\nprogram\n .command('init')\n .description('Create a default .env configuration file')\n .action(async () => {\n await initHandler({} as any); // Handler expects argv, but doesn't use it\n });\n\nprogram\n .command('md <url>')\n .description('Convert a Notion page to markdown')\n .option('-o, --output <dir>', 'Output directory', './nconv-output')\n .option('-i, --image-dir <dir>', 'Image folder name (relative to output)', 'images')\n .option('-f, --filename <name>', '출력 파일명 (확장자 제외 또는 포함)')\n .option('-v, --verbose', 'Enable verbose logging', false)\n .action(async (url: string, options) => {\n await mdCommand(url, {\n output: options.output,\n imageDir: options.imageDir,\n filename: options.filename,\n verbose: options.verbose,\n });\n });\n\nprogram\n .command('html <url>')\n .description('Convert a Notion page to HTML')\n .option('-o, --output <dir>', 'Output directory', './nconv-output')\n .option('-i, --image-dir <dir>', 'Image folder name (relative to output)', 'images')\n .option('-f, --filename <name>', 'Output filename (without extension or with)')\n .option('-v, --verbose', 'Enable verbose logging', false)\n .action(async (url: string, options) => {\n await htmlCommand(url, {\n output: options.output,\n imageDir: options.imageDir,\n filename: options.filename,\n verbose: options.verbose,\n });\n });\n\nprogram\n .command('pdf <url>')\n .description('Convert a Notion page to PDF (renders actual Notion page)')\n .option('-o, --output <dir>', 'Output directory', './nconv-output')\n .option('-f, --filename <name>', 'Output filename (without extension or with)')\n .option('-v, --verbose', 'Enable verbose logging', false)\n .action(async (url: string, options) => {\n await pdfCommand(url, {\n output: options.output,\n imageDir: 'images', // Not used for PDF, but required by interface\n filename: options.filename,\n verbose: options.verbose,\n });\n });\n\n// 개발 환경에서만 debug 명령어를 활성화\nif (process.env.NODE_ENV !== 'production') {\n program\n .command('debug <url>')\n .description('Debug: Output raw markdown and image URLs')\n .option('-o, --output <dir>', '출력 디렉토리', './nconv-output')\n .option('-i, --image-dir <dir>', 'Image folder name', 'images')\n .option('-v, --verbose', 'Enable verbose logging', false)\n .action(async (url: string, options) => {\n await debugCommand(url, {\n output: options.output,\n imageDir: options.imageDir,\n filename: '',\n verbose: options.verbose,\n });\n });\n}\n\n// If no arguments provided, start REPL mode\n(async () => {\n if (process.argv.length === 2) {\n await startRepl();\n } else {\n program.parse();\n }\n})();\n","import { config as dotenvConfig } from 'dotenv';\nimport { existsSync, readFileSync } from 'fs';\nimport { resolve, join } from 'path';\nimport os from 'os';\nimport * as logger from './utils/logger.js';\n\n// Disable dotenv default logging\nprocess.env.DOTENV_CONFIG_SILENT = 'true';\n\nexport interface NotionConfig {\n tokenV2: string;\n fileToken: string;\n}\n\nexport interface ConverterOptions {\n output: string;\n imageDir: string;\n filename?: string;\n verbose: boolean;\n}\n\nexport interface FullConfig extends NotionConfig, ConverterOptions {}\n\nfunction getConfigPath(): string {\n return join(os.homedir(), '.nconv', '.env');\n}\n\n/**\n * Loads environment variables from the global config file.\n */\nfunction loadEnv() {\n const configPath = getConfigPath();\n\n if (!existsSync(configPath)) {\n return;\n }\n\n try {\n // Manually parse .env file to avoid dotenv logging\n const content = readFileSync(configPath, 'utf-8');\n let loadedCount = 0;\n\n content.split('\\n').forEach((line) => {\n const trimmed = line.trim();\n\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) {\n return;\n }\n\n // Parse key=value\n const equalIndex = trimmed.indexOf('=');\n if (equalIndex === -1) {\n return;\n }\n\n const key = trimmed.substring(0, equalIndex).trim();\n const value = trimmed.substring(equalIndex + 1).trim();\n\n if (key && value) {\n process.env[key] = value;\n loadedCount++;\n }\n });\n\n if (process.env.NCONV_VERBOSE) {\n logger.info(`✓ Loaded ${loadedCount} environment variable(s) from ${configPath}`);\n }\n } catch (error) {\n logger.error(`Failed to load configuration from: ${configPath}`);\n if (error instanceof Error) {\n logger.error(error.message);\n }\n }\n}\n\n/**\n * Checks if the required environment variables are set.\n * Exits the process if tokens are missing (for CLI commands).\n */\nfunction checkEnv() {\n const configPath = getConfigPath();\n if (!process.env.TOKEN_V2 || !process.env.FILE_TOKEN) {\n logger.error('Notion tokens are not set.');\n if (!existsSync(configPath)) {\n logger.error('Configuration file not found.');\n logger.error('Please run \"nconv init\" to create a configuration file.');\n } else {\n logger.error(`Please set TOKEN_V2 and FILE_TOKEN in: ${configPath}`);\n }\n process.exit(1);\n }\n}\n\n/**\n * Validates if the required environment variables are set.\n * Returns true if valid, false otherwise (for REPL mode).\n */\nexport function validateConfig(): { valid: boolean; message?: string } {\n loadEnv();\n const configPath = getConfigPath();\n\n if (!process.env.TOKEN_V2 || !process.env.FILE_TOKEN) {\n if (!existsSync(configPath)) {\n return {\n valid: false,\n message: 'Configuration file not found. Please run /init to set up your Notion tokens.',\n };\n } else {\n return {\n valid: false,\n message: `Notion tokens are not set. Please run /init or /config to set up your tokens.\\nConfig file: ${configPath}`,\n };\n }\n }\n\n return { valid: true };\n}\n\n/**\n * Notion 인증 설정 가져오기\n */\nexport function getNotionConfig(): NotionConfig {\n const tokenV2 = process.env.TOKEN_V2 || '';\n const fileToken = process.env.FILE_TOKEN || '';\n return { tokenV2, fileToken };\n}\n\n/**\n * 전체 설정 생성\n */\nexport function createConfig(options: ConverterOptions): FullConfig {\n loadEnv();\n checkEnv();\n\n const notionConfig = getNotionConfig();\n\n // 출력 디렉토리 절대 경로로 변환\n const outputDir = resolve(process.cwd(), options.output);\n\n return {\n ...notionConfig,\n ...options,\n output: outputDir,\n };\n}\n\n/**\n * 디렉토리 존재 여부 확인\n */\nexport function ensureDirectoryExists(dir: string): boolean {\n return existsSync(dir);\n}\n","import chalk from 'chalk';\nimport ora, { Ora } from 'ora';\n\n/**\n * print success log\n */\nexport function success(message: string): void {\n console.log(chalk.green('✓'), message);\n}\n\n/**\n * print error log\n */\nexport function error(message: string): void {\n console.error(chalk.red('✗'), message);\n}\n\n/**\n * print info log\n */\nexport function info(message: string): void {\n console.log(chalk.blue('ℹ'), message);\n}\n\n/**\n * print warn log\n */\nexport function warn(message: string): void {\n console.log(chalk.yellow('⚠'), message);\n}\n\n/**\n * print spinner\n */\nexport function spinner(text: string): Ora {\n return ora(text).start();\n}\n","import { NotionExporter } from 'notion-exporter';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { mdToPdf } from 'md-to-pdf';\nimport type { NotionConfig } from '../config.js';\n\nexport interface ExportResult {\n markdown: string;\n imageFiles: Array<{ filename: string; sourcePath: string }>;\n}\n\nexport interface HTMLExportResult {\n html: string;\n imageFiles: Array<{ filename: string; sourcePath: string }>;\n}\n\nexport interface PDFExportResult {\n pdfPath: string;\n filename: string;\n}\n\nexport interface MarkdownPDFOptions {\n format?: 'A4' | 'Letter';\n margin?: {\n top?: string;\n right?: string;\n bottom?: string;\n left?: string;\n };\n stylesheet?: string | string[];\n}\n\n/**\n * Notion 페이지를 마크다운으로 내보내기\n */\nexport class NotionMarkdownExporter {\n private exporter: NotionExporter;\n\n constructor(config: NotionConfig, exportType: 'markdown' | 'html' | 'pdf' = 'markdown') {\n this.exporter = new NotionExporter(config.tokenV2, config.fileToken, {\n exportType,\n });\n }\n\n /**\n * Notion URL에서 마크다운과 이미지 파일 가져오기\n */\n async exportWithImages(notionUrl: string, tempDir: string): Promise<ExportResult> {\n try {\n // 임시 디렉토리 생성\n await fs.mkdir(tempDir, { recursive: true });\n\n // getMdFiles로 마크다운과 이미지를 함께 다운로드\n await this.exporter.getMdFiles(notionUrl, tempDir);\n\n // 다운로드된 파일 목록 가져오기\n const files = await fs.readdir(tempDir, { withFileTypes: true });\n\n // 마크다운 파일 찾기\n const mdFile = files.find(f => f.isFile() && f.name.endsWith('.md'));\n if (!mdFile) {\n throw new Error('Markdown file not found.');\n }\n\n // 마크다운 내용 읽기\n const mdPath = path.join(tempDir, mdFile.name);\n const markdown = await fs.readFile(mdPath, 'utf-8');\n\n // 이미지 파일 목록 (마크다운이 아닌 파일들)\n const imageFiles = files\n .filter(f => f.isFile() && !f.name.endsWith('.md'))\n .map(f => ({\n filename: f.name,\n sourcePath: path.join(tempDir, f.name),\n }));\n\n // 디렉토리 내 폴더도 확인 (Notion이 폴더 구조로 저장할 수 있음)\n const dirs = files.filter(f => f.isDirectory());\n for (const dir of dirs) {\n const subFiles = await fs.readdir(path.join(tempDir, dir.name), { withFileTypes: true });\n for (const subFile of subFiles) {\n if (subFile.isFile() && !subFile.name.endsWith('.md')) {\n imageFiles.push({\n filename: path.join(dir.name, subFile.name),\n sourcePath: path.join(tempDir, dir.name, subFile.name),\n });\n }\n }\n }\n\n return { markdown, imageFiles };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to fetch Notion page: ${error.message}`);\n }\n throw new Error('Failed to fetch Notion page.');\n }\n }\n\n /**\n * Notion URL에서 HTML과 이미지 파일 가져오기\n */\n async exportHTML(notionUrl: string, tempDir: string): Promise<HTMLExportResult> {\n try {\n // 임시 디렉토리 생성\n await fs.mkdir(tempDir, { recursive: true });\n\n // getMdFiles로 HTML과 이미지를 함께 다운로드\n await this.exporter.getMdFiles(notionUrl, tempDir);\n\n // 다운로드된 파일 목록 가져오기\n const files = await fs.readdir(tempDir, { withFileTypes: true });\n\n // HTML 파일 찾기\n const htmlFile = files.find(f => f.isFile() && f.name.endsWith('.html'));\n if (!htmlFile) {\n throw new Error('HTML file not found.');\n }\n\n // HTML 내용 읽기\n const htmlPath = path.join(tempDir, htmlFile.name);\n const html = await fs.readFile(htmlPath, 'utf-8');\n\n // 이미지 파일 목록 (HTML이 아닌 파일들)\n const imageFiles = files\n .filter(f => f.isFile() && !f.name.endsWith('.html'))\n .map(f => ({\n filename: f.name,\n sourcePath: path.join(tempDir, f.name),\n }));\n\n // 디렉토리 내 폴더도 확인 (Notion이 폴더 구조로 저장할 수 있음)\n const dirs = files.filter(f => f.isDirectory());\n for (const dir of dirs) {\n const subFiles = await fs.readdir(path.join(tempDir, dir.name), { withFileTypes: true });\n for (const subFile of subFiles) {\n if (subFile.isFile() && !subFile.name.endsWith('.html')) {\n imageFiles.push({\n filename: path.join(dir.name, subFile.name),\n sourcePath: path.join(tempDir, dir.name, subFile.name),\n });\n }\n }\n }\n\n return { html, imageFiles };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to fetch Notion page as HTML: ${error.message}`);\n }\n throw new Error('Failed to fetch Notion page as HTML.');\n }\n }\n\n /**\n * Markdown을 PDF로 변환 (md-to-pdf 사용)\n * GitHub 스타일의 깔끔한 PDF 생성\n */\n async exportMarkdownToPDF(\n markdownContent: string,\n outputPath: string,\n options: MarkdownPDFOptions = {}\n ): Promise<void> {\n try {\n const pdfOptions = {\n content: markdownContent,\n stylesheet: options.stylesheet || [\n 'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css',\n ],\n body_class: 'markdown-body',\n css: `\n .markdown-body {\n box-sizing: border-box;\n min-width: 200px;\n max-width: 980px;\n margin: 0 auto;\n padding: 45px;\n }\n\n @media (max-width: 767px) {\n .markdown-body {\n padding: 15px;\n }\n }\n\n /* 코드 블록 스타일 개선 */\n .markdown-body pre {\n background-color: #f6f8fa;\n border-radius: 6px;\n padding: 16px;\n overflow: auto;\n }\n\n .markdown-body code {\n background-color: rgba(175, 184, 193, 0.2);\n border-radius: 6px;\n padding: 0.2em 0.4em;\n }\n\n /* 이미지 스타일 */\n .markdown-body img {\n max-width: 100%;\n height: auto;\n }\n `,\n pdf_options: {\n format: options.format || 'A4',\n margin: options.margin || {\n top: '20mm',\n right: '20mm',\n bottom: '20mm',\n left: '20mm',\n },\n printBackground: true,\n },\n };\n\n const result = await mdToPdf(pdfOptions);\n\n if (result.content) {\n await fs.writeFile(outputPath, result.content);\n } else {\n throw new Error('PDF content is empty');\n }\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to convert Markdown to PDF: ${error.message}`);\n }\n throw new Error('Failed to convert Markdown to PDF.');\n }\n }\n}\n","import { promises as fs } from 'fs';\nimport path from 'path';\nimport slugify from 'slugify';\nimport { v4 as uuidv4 } from 'uuid';\n\n/**\n * Notion URL에서 페이지 제목 추출 (간단한 버전)\n */\nexport function extractTitleFromUrl(notionUrl: string): string | null {\n try {\n const url = new URL(notionUrl);\n const pathname = url.pathname;\n\n // /Page-Title-abc123 형식에서 제목 부분 추출\n const match = pathname.match(/\\/([^/]+)-[a-f0-9]{32}$/i);\n if (match) {\n const title = match[1].replace(/-/g, ' ');\n return title;\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * 안전한 파일명 생성\n */\nexport function generateSafeFilename(title: string | null, extension: string = 'md'): string {\n if (title) {\n const slug = slugify(title, {\n lower: true,\n strict: true,\n locale: 'ko',\n });\n\n if (slug.length > 0) {\n return extension ? `${slug}.${extension}` : slug;\n }\n }\n\n // 제목이 없거나 slugify 실패 시 UUID 사용\n const hash = uuidv4().slice(0, 8);\n return extension ? `notion-export-${hash}.${extension}` : `notion-export-${hash}`;\n}\n\n/**\n * 마크다운 파일 저장\n */\nexport async function saveMarkdownFile(\n outputDir: string,\n filename: string,\n content: string\n): Promise<string> {\n await fs.mkdir(outputDir, { recursive: true });\n\n const filePath = path.join(outputDir, filename);\n await fs.writeFile(filePath, content, 'utf-8');\n\n return filePath;\n}\n\n/**\n * 파일 경로를 상대 경로로 변환\n */\nexport function toRelativePath(from: string, to: string): string {\n return path.relative(from, to);\n}\n","import { ConverterOptions, createConfig } from '../config.js';\nimport { NotionMarkdownExporter } from '../core/exporter.js';\nimport { generateSafeFilename, extractTitleFromUrl, saveMarkdownFile } from '../utils/file.js';\nimport * as logger from '../utils/logger.js';\nimport path from 'path';\nimport { promises as fs } from 'fs';\nimport os from 'os';\n\n/**\n * md 명령어 핸들러\n */\nexport async function mdCommand(notionUrl: string, options: ConverterOptions) {\n const tempDir = path.join(os.tmpdir(), `nconv-cli-${Date.now()}`);\n\n try {\n // 설정 생성\n const config = createConfig(options);\n\n if (config.verbose) {\n logger.info('Configuration loaded successfully');\n console.log(` Output directory: ${config.output}\\n`);\n }\n\n // 1. Notion에서 마크다운과 이미지 가져오기\n const spinner = logger.spinner('Fetching Notion page...');\n\n const exporter = new NotionMarkdownExporter({\n tokenV2: config.tokenV2,\n fileToken: config.fileToken,\n });\n\n let result;\n try {\n result = await exporter.exportWithImages(notionUrl, tempDir);\n spinner.succeed(`Notion page fetched (${result.imageFiles.length} images)`);\n } catch (error) {\n spinner.fail('Failed to fetch Notion page');\n throw error;\n }\n\n // 2. 파일명/폴더명 생성\n let baseFilename: string;\n if (config.filename) {\n baseFilename = config.filename.replace(/\\.md$/, '');\n } else {\n const title = extractTitleFromUrl(notionUrl);\n baseFilename = generateSafeFilename(title, ''); // 확장자 없이 생성\n }\n\n // 3. 제목별 폴더 생성 (output/제목/)\n const pageDir = path.join(config.output, baseFilename);\n await fs.mkdir(pageDir, { recursive: true });\n\n // 4. 이미지 폴더 생성 및 이미지 파일 이동\n const imageOutputDir = path.join(pageDir, config.imageDir);\n await fs.mkdir(imageOutputDir, { recursive: true });\n\n if (config.verbose && result.imageFiles.length > 0) {\n console.log(`Processing image files...\\n`);\n }\n\n let processedMarkdown = result.markdown;\n for (const imageFile of result.imageFiles) {\n try {\n // 파일명에서 공백을 하이픈으로 변경 (마크다운 호환성)\n const originalFileName = path.basename(imageFile.filename);\n const safeFileName = originalFileName.replace(/\\s+/g, '-');\n\n // 이미지 파일 복사\n const targetPath = path.join(imageOutputDir, safeFileName);\n await fs.copyFile(imageFile.sourcePath, targetPath);\n\n if (config.verbose) {\n console.log(`✓ ${safeFileName}`);\n }\n\n // 마크다운 내 경로를 상대경로로 변경\n const originalPath = imageFile.filename;\n const relativePath = `./${config.imageDir}/${safeFileName}`;\n\n // Notion이 URL 인코딩하는 방식: 각 경로 부분을 개별적으로 인코딩\n const pathParts = originalPath.split('/');\n const encodedPath = pathParts.map(part => encodeURIComponent(part)).join('/');\n\n // 정규식 특수문자 이스케이프 함수\n const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n // 모든 가능한 형태의 경로를 교체\n processedMarkdown = processedMarkdown\n .replace(new RegExp(`\\\\(${escapeRegex(originalPath)}\\\\)`, 'g'), `(${relativePath})`)\n .replace(new RegExp(`\\\\(${escapeRegex(encodedPath)}\\\\)`, 'g'), `(${relativePath})`);\n\n } catch (error) {\n if (config.verbose) {\n const errorMsg = error instanceof Error ? error.message : '알 수 없는 오류';\n console.error(`✗ ${imageFile.filename}: ${errorMsg}`);\n }\n }\n }\n\n // 5. 마크다운 파일 저장 (제목 폴더 안에)\n const filename = `${baseFilename}.md`;\n const filePath = await saveMarkdownFile(pageDir, filename, processedMarkdown);\n\n // 6. 결과 출력\n console.log('');\n logger.success('Conversion complete!');\n console.log('');\n console.log(`📁 Folder: ${path.relative(process.cwd(), pageDir)}`);\n console.log(`📄 Markdown: ${filename}`);\n\n if (result.imageFiles.length > 0) {\n console.log(`🖼️ Images: ${config.imageDir}/ (${result.imageFiles.length} files)`);\n }\n\n console.log('');\n\n } catch (error) {\n if (error instanceof Error) {\n logger.error(error.message);\n } else {\n logger.error('An unknown error occurred.');\n }\n process.exit(1);\n } finally {\n // 임시 디렉토리 정리\n try {\n await fs.rm(tempDir, { recursive: true, force: true });\n } catch {\n // 무시\n }\n }\n}\n","import { ConverterOptions, createConfig } from '../config.js';\nimport { NotionMarkdownExporter } from '../core/exporter.js';\nimport { generateSafeFilename, extractTitleFromUrl } from '../utils/file.js';\nimport * as logger from '../utils/logger.js';\nimport path from 'path';\nimport { promises as fs } from 'fs';\nimport os from 'os';\n\n/**\n * html 명령어 핸들러\n */\nexport async function htmlCommand(notionUrl: string, options: ConverterOptions) {\n const tempDir = path.join(os.tmpdir(), `nconv-cli-${Date.now()}`);\n\n try {\n // 설정 생성\n const config = createConfig(options);\n\n if (config.verbose) {\n logger.info('Configuration loaded successfully');\n console.log(` Output directory: ${config.output}\\n`);\n }\n\n // 1. Notion에서 HTML과 이미지 가져오기\n const spinner = logger.spinner('Fetching Notion page as HTML...');\n\n const exporter = new NotionMarkdownExporter({\n tokenV2: config.tokenV2,\n fileToken: config.fileToken,\n }, 'html');\n\n let result;\n try {\n result = await exporter.exportHTML(notionUrl, tempDir);\n spinner.succeed(`Notion page fetched (${result.imageFiles.length} images)`);\n } catch (error) {\n spinner.fail('Failed to fetch Notion page');\n throw error;\n }\n\n // 2. 파일명/폴더명 생성\n let baseFilename: string;\n if (config.filename) {\n baseFilename = config.filename.replace(/\\.html?$/, '');\n } else {\n const title = extractTitleFromUrl(notionUrl);\n baseFilename = generateSafeFilename(title, ''); // 확장자 없이 생성\n }\n\n // 3. 제목별 폴더 생성 (output/제목/)\n const pageDir = path.join(config.output, baseFilename);\n await fs.mkdir(pageDir, { recursive: true });\n\n // 4. 이미지 폴더 생성 및 이미지 파일 이동\n const imageOutputDir = path.join(pageDir, config.imageDir);\n await fs.mkdir(imageOutputDir, { recursive: true });\n\n if (config.verbose && result.imageFiles.length > 0) {\n console.log(`Processing image files...\\n`);\n }\n\n let processedHtml = result.html;\n for (const imageFile of result.imageFiles) {\n try {\n // 파일명에서 공백을 하이픈으로 변경 (호환성)\n const originalFileName = path.basename(imageFile.filename);\n const safeFileName = originalFileName.replace(/\\s+/g, '-');\n\n // 이미지 파일 복사\n const targetPath = path.join(imageOutputDir, safeFileName);\n await fs.copyFile(imageFile.sourcePath, targetPath);\n\n if (config.verbose) {\n console.log(`✓ ${safeFileName}`);\n }\n\n // HTML 내 경로를 상대경로로 변경\n const originalPath = imageFile.filename;\n const relativePath = `./${config.imageDir}/${safeFileName}`;\n\n // Notion이 URL 인코딩하는 방식: 각 경로 부분을 개별적으로 인코딩\n const pathParts = originalPath.split('/');\n const encodedPath = pathParts.map(part => encodeURIComponent(part)).join('/');\n\n // 정규식 특수문자 이스케이프 함수\n const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n // HTML에서 모든 가능한 형태의 경로를 교체\n // src=\"...\" 형태와 href=\"...\" 형태 모두 처리\n processedHtml = processedHtml\n .replace(new RegExp(`(src|href)=\"${escapeRegex(originalPath)}\"`, 'g'), `$1=\"${relativePath}\"`)\n .replace(new RegExp(`(src|href)=\"${escapeRegex(encodedPath)}\"`, 'g'), `$1=\"${relativePath}\"`);\n\n } catch (error) {\n if (config.verbose) {\n const errorMsg = error instanceof Error ? error.message : '알 수 없는 오류';\n console.error(`✗ ${imageFile.filename}: ${errorMsg}`);\n }\n }\n }\n\n // 5. HTML 파일 저장 (제목 폴더 안에)\n const filename = `${baseFilename}.html`;\n const filePath = path.join(pageDir, filename);\n await fs.writeFile(filePath, processedHtml, 'utf-8');\n\n // 6. 결과 출력\n console.log('');\n logger.success('Conversion complete!');\n console.log('');\n console.log(`📁 Folder: ${path.relative(process.cwd(), pageDir)}`);\n console.log(`📄 HTML: ${filename}`);\n\n if (result.imageFiles.length > 0) {\n console.log(`🖼️ Images: ${config.imageDir}/ (${result.imageFiles.length} files)`);\n }\n\n console.log('');\n\n } catch (error) {\n if (error instanceof Error) {\n logger.error(error.message);\n } else {\n logger.error('An unknown error occurred.');\n }\n process.exit(1);\n } finally {\n // 임시 디렉토리 정리\n try {\n await fs.rm(tempDir, { recursive: true, force: true });\n } catch {\n // 무시\n }\n }\n}\n","import { ConverterOptions, createConfig } from '../config.js';\nimport { NotionMarkdownExporter } from '../core/exporter.js';\nimport { generateSafeFilename, extractTitleFromUrl } from '../utils/file.js';\nimport * as logger from '../utils/logger.js';\nimport path from 'path';\nimport { promises as fs } from 'fs';\nimport os from 'os';\n\n/**\n * pdf 명령어 핸들러\n * Notion → Markdown → PDF 플로우로 깔끔한 PDF 생성\n */\nexport async function pdfCommand(notionUrl: string, options: ConverterOptions) {\n const tempDir = path.join(os.tmpdir(), `nconv-cli-${Date.now()}`);\n\n try {\n // 설정 생성\n const config = createConfig(options);\n\n if (config.verbose) {\n logger.info('Configuration loaded successfully');\n console.log(` Output directory: ${config.output}\\n`);\n }\n\n // 1. Notion에서 Markdown 가져오기\n const spinner = logger.spinner('Fetching Notion page as Markdown...');\n\n const exporter = new NotionMarkdownExporter({\n tokenV2: config.tokenV2,\n fileToken: config.fileToken,\n });\n\n let result;\n try {\n result = await exporter.exportWithImages(notionUrl, tempDir);\n spinner.succeed(`Notion page fetched (${result.imageFiles.length} images)`);\n } catch (error) {\n spinner.fail('Failed to fetch Notion page');\n throw error;\n }\n\n // 2. 파일명/폴더명 생성\n let baseFilename: string;\n if (config.filename) {\n baseFilename = config.filename.replace(/\\.pdf$/, '');\n } else {\n const title = extractTitleFromUrl(notionUrl);\n baseFilename = generateSafeFilename(title, '');\n }\n\n // 3. 제목별 폴더 생성 (output/제목/)\n const pageDir = path.join(config.output, baseFilename);\n await fs.mkdir(pageDir, { recursive: true });\n\n // 4. 이미지 폴더 생성 및 이미지 파일 이동\n const imageOutputDir = path.join(pageDir, config.imageDir);\n await fs.mkdir(imageOutputDir, { recursive: true });\n\n if (config.verbose && result.imageFiles.length > 0) {\n console.log(`Processing image files...\\n`);\n }\n\n let processedMarkdown = result.markdown;\n for (const imageFile of result.imageFiles) {\n try {\n const originalFileName = path.basename(imageFile.filename);\n const safeFileName = originalFileName.replace(/\\s+/g, '-');\n\n // 이미지 파일 복사\n const targetPath = path.join(imageOutputDir, safeFileName);\n await fs.copyFile(imageFile.sourcePath, targetPath);\n\n if (config.verbose) {\n console.log(`✓ ${safeFileName}`);\n }\n\n // PDF용: 이미지를 base64로 변환\n const imageBuffer = await fs.readFile(targetPath);\n const base64 = imageBuffer.toString('base64');\n\n // 파일 확장자에서 MIME 타입 결정\n const ext = path.extname(safeFileName).slice(1).toLowerCase();\n const mimeType = ext === 'jpg' ? 'jpeg' : ext;\n const dataUrl = `data:image/${mimeType};base64,${base64}`;\n\n // Markdown 내 경로를 처리\n const originalPath = imageFile.filename;\n const relativePath = `./${config.imageDir}/${safeFileName}`;\n\n const pathParts = originalPath.split('/');\n const encodedPath = pathParts.map(part => encodeURIComponent(part)).join('/');\n\n const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n // PDF용 Markdown: base64 data URL 사용\n processedMarkdown = processedMarkdown\n .replace(new RegExp(`\\\\(${escapeRegex(originalPath)}\\\\)`, 'g'), `(${dataUrl})`)\n .replace(new RegExp(`\\\\(${escapeRegex(encodedPath)}\\\\)`, 'g'), `(${dataUrl})`);\n\n } catch (error) {\n if (config.verbose) {\n const errorMsg = error instanceof Error ? error.message : 'Unknown error';\n console.error(`✗ ${imageFile.filename}: ${errorMsg}`);\n }\n }\n }\n\n // 5. Markdown → PDF 변환\n const pdfSpinner = logger.spinner('Converting Markdown to PDF...');\n\n const filename = `${baseFilename}.pdf`;\n const pdfPath = path.join(pageDir, filename);\n\n try {\n await exporter.exportMarkdownToPDF(processedMarkdown, pdfPath, {\n format: 'A4',\n });\n pdfSpinner.succeed('PDF generated successfully');\n } catch (error) {\n pdfSpinner.fail('Failed to generate PDF');\n throw error;\n }\n\n // 6. 결과 출력\n console.log('');\n logger.success('PDF export complete!');\n console.log('');\n console.log(`📁 Folder: ${path.relative(process.cwd(), pageDir)}`);\n console.log(`📄 PDF: ${filename}`);\n if (result.imageFiles.length > 0) {\n console.log(`🖼️ Images: ${config.imageDir}/ (${result.imageFiles.length} files)`);\n }\n console.log('');\n\n } catch (error) {\n if (error instanceof Error) {\n logger.error(error.message);\n } else {\n logger.error('An unknown error occurred.');\n }\n process.exit(1);\n } finally {\n // 임시 디렉토리 정리\n try {\n await fs.rm(tempDir, { recursive: true, force: true });\n } catch {\n // 무시\n }\n }\n}\n","import { ConverterOptions, createConfig } from '../config.js';\nimport { NotionMarkdownExporter } from '../core/exporter.js';\nimport * as fs from 'fs/promises';\nimport * as os from 'os';\nimport * as path from 'path';\n\n/**\n * Debugging command - output raw markdown\n */\nexport async function debugCommand(notionUrl: string, options: ConverterOptions) {\n let tempDir: string | undefined;\n try {\n const config = createConfig(options);\n\n const exporter = new NotionMarkdownExporter({\n tokenV2: config.tokenV2,\n fileToken: config.fileToken,\n });\n\n tempDir = path.join(os.tmpdir(), `notion-debug-${Date.now()}`); // Create temporary directory\n\n console.log('Fetching Notion page...\\n');\n const { markdown, imageFiles } = await exporter.exportWithImages(notionUrl, tempDir);\n\n console.log('=== Raw Markdown ===\\n');\n console.log(markdown.slice(0, 3000));\n console.log('\\n... (truncated) ...\\n');\n\n console.log(`\\n=== Found Images (${imageFiles.length}) ===\\n`);\n imageFiles.forEach((file, i) => {\n console.log(`${i + 1}. ${file.filename} (원본 경로: ${file.sourcePath})`);\n });\n\n } catch (error) {\n console.error('Error:', error);\n } finally {\n if (tempDir) {\n // Clean up temporary directory\n await fs.rm(tempDir, { recursive: true, force: true }).catch(err => {\n console.warn(`Error cleaning up temporary directory: ${err.message}`);\n });\n }\n }\n}","\nimport type { Arguments, CommandBuilder } from 'yargs';\nimport fs from 'fs';\nimport path from 'path';\nimport os from 'os';\nimport * as logger from '../utils/logger.js';\n\ntype Options = {\n // No options yet\n};\n\nexport const command: string = 'init';\nexport const desc: string = 'Create a default .env configuration file in your home directory.';\n\nexport const builder: CommandBuilder<Options, Options> = (yargs) =>\n yargs\n .example([\n ['$0 init', 'Create the default configuration file.'],\n ]);\n\nexport const handler = async (argv: Arguments<Options>): Promise<void> => {\n const configDir = path.join(os.homedir(), '.nconv');\n const configFile = path.join(configDir, '.env');\n\n logger.info(`Checking for config file at: ${configFile}`);\n\n if (fs.existsSync(configFile)) {\n logger.warn('Configuration file already exists.');\n logger.warn(`If you want to re-initialize, please delete the file first: ${configFile}`);\n return;\n }\n\n const envContent = `\n# Please provide your Notion access tokens.\n# These are required to fetch content from your Notion pages.\n# You can find these tokens in your browser's cookies when you are logged into Notion.\nTOKEN_V2=\nFILE_TOKEN=\n`.trim();\n\n try {\n logger.info(`Creating directory at: ${configDir}`);\n if (!fs.existsSync(configDir)) {\n fs.mkdirSync(configDir, { recursive: true });\n }\n fs.writeFileSync(configFile, envContent);\n logger.info('✅ Successfully created configuration file.');\n logger.info(`Please edit the file to set your environment variables: ${configFile}`);\n } catch (error) {\n logger.error('Failed to create configuration file.');\n if (error instanceof Error) {\n logger.error(error.message);\n }\n }\n};\n","import prompts from 'prompts';\nimport chalk from 'chalk';\nimport * as logger from '../utils/logger.js';\nimport { executeCommand, getCommandChoices } from './commands.js';\n\n/**\n * Display a centered banner with dynamic box sizing\n */\nfunction showBanner(): void {\n const title = 'NCONV CLI (Notion Converter CLI)';\n const padding = 2;\n const totalWidth = title.length + (padding * 2);\n\n const topBorder = '╔' + '═'.repeat(totalWidth) + '╗';\n const bottomBorder = '╚' + '═'.repeat(totalWidth) + '╝';\n\n console.log(chalk.cyan('\\n' + topBorder));\n console.log(chalk.cyan('║') + chalk.bold(' '.repeat(padding) + title + ' '.repeat(padding)) + chalk.cyan('║'));\n console.log(chalk.cyan(bottomBorder + '\\n'));\n\n logger.info('Welcome to nconv interactive mode!');\n logger.info('Type /help to see available commands');\n logger.info('Type /exit to quit\\n');\n\n // Show quick examples\n console.log(chalk.dim('Quick examples:'));\n console.log(chalk.dim(' /init - Set up Notion tokens'));\n console.log(chalk.dim(' /md <url> - Convert Notion page'));\n console.log(chalk.dim(' /md <url> -o ./blog -f my-post - Convert with options\\n'));\n}\n\n/**\n * Start the REPL loop\n */\nexport async function startRepl(): Promise<void> {\n showBanner();\n\n let shouldExit = false;\n\n while (!shouldExit) {\n try {\n const response = await prompts({\n type: 'autocomplete',\n name: 'command',\n message: chalk.cyan('nconv'),\n choices: getCommandChoices(),\n suggest: async (input: string, choices: any[]) => {\n // If input doesn't start with /, add it for search\n const searchInput = input.startsWith('/') ? input : `/${input}`;\n\n // Filter choices based on input\n const filtered = choices.filter((choice: any) =>\n choice.title.toLowerCase().startsWith(searchInput.toLowerCase())\n );\n\n // If user typed a full command or URL, allow it\n if (filtered.length === 0 && input.trim()) {\n return Promise.resolve([{ title: input, value: input }]);\n }\n\n return Promise.resolve(filtered);\n },\n limit: 5,\n });\n\n if (response.command === undefined) {\n // User cancelled (Ctrl+C)\n console.log('\\n');\n logger.info('Goodbye! 👋');\n break;\n }\n\n shouldExit = await executeCommand(response.command);\n\n if (!shouldExit) {\n console.log(); // Empty line for readability\n }\n } catch (error) {\n if (error instanceof Error) {\n logger.error(`Error: ${error.message}`);\n console.log(); // Empty line for readability\n }\n }\n }\n\n logger.info('Exiting nconv...');\n}\n","import { input, confirm } from '@inquirer/prompts';\nimport * as logger from '../utils/logger.js';\nimport { mdCommand } from '../commands/md.js';\nimport { htmlCommand } from '../commands/html.js';\nimport { pdfCommand } from '../commands/pdf.js';\nimport { validateConfig } from '../config.js';\nimport {\n loadConfig,\n saveConfig,\n promptInitConfig,\n promptEditConfig,\n} from './prompts.js';\n\n/**\n * Handle /init command - Initialize configuration\n */\nexport async function handleInit(): Promise<void> {\n const existing = loadConfig();\n const isFirstTime = !existing || (!existing.TOKEN_V2 && !existing.FILE_TOKEN);\n\n // If config already exists, ask for confirmation\n if (!isFirstTime) {\n logger.warn('Configuration already exists.');\n\n try {\n const overwrite = await confirm({\n message: 'Do you want to overwrite the existing configuration?',\n default: false,\n });\n\n if (!overwrite) {\n logger.info('Configuration unchanged. Use /config to view or edit your settings.');\n return;\n }\n } catch (error) {\n if (error instanceof Error && error.message === 'User force closed the prompt') {\n logger.warn('\\nCancelled.');\n }\n return;\n }\n }\n\n // Show detailed instructions for token setup\n logger.info('\\n📝 How to find your Notion tokens\\n');\n logger.info('1. Log in to https://notion.so in your browser');\n logger.info('2. Open browser developer tools (press F12)');\n logger.info('3. Go to the \"Application\" tab');\n logger.info('4. Find \"Cookies\" section and select https://www.notion.so');\n logger.info('5. Copy the value of \"token_v2\" cookie → TOKEN_V2');\n logger.info('6. Copy the value of \"file_token\" cookie → FILE_TOKEN\\n');\n\n try {\n const config = await promptInitConfig();\n saveConfig(config);\n } catch (error) {\n if (error instanceof Error && error.message === 'User force closed the prompt') {\n logger.warn('\\nConfiguration cancelled.');\n } else {\n throw error;\n }\n }\n}\n\n/**\n * Handle /config command - View and edit configuration\n */\nexport async function handleConfig(): Promise<void> {\n const existing = loadConfig();\n\n if (!existing || (!existing.TOKEN_V2 && !existing.FILE_TOKEN)) {\n logger.warn('No configuration found.');\n logger.info('Run /init to create initial configuration.');\n return;\n }\n\n logger.info('Current configuration:');\n logger.info(`TOKEN_V2: ${existing.TOKEN_V2 ? '***' + existing.TOKEN_V2.slice(-8) : '(not set)'}`);\n logger.info(`FILE_TOKEN: ${existing.FILE_TOKEN ? '***' + existing.FILE_TOKEN.slice(-8) : '(not set)'}\\n`);\n\n try {\n const edit = await input({\n message: 'Edit configuration? (y/n)',\n default: 'n',\n });\n\n if (edit.toLowerCase() === 'y' || edit.toLowerCase() === 'yes') {\n const newConfig = await promptEditConfig(existing);\n saveConfig(newConfig);\n }\n } catch (error) {\n if (error instanceof Error && error.message === 'User force closed the prompt') {\n logger.warn('\\nEdit cancelled.');\n } else {\n throw error;\n }\n }\n}\n\n/**\n * Handle /md command - Convert Notion page to markdown\n */\nexport async function handleMd(args: string[]): Promise<void> {\n let url = '';\n const options: any = {\n output: './nconv-output',\n imageDir: 'images',\n verbose: false,\n };\n\n // Interactive mode (TUI) - step by step prompts\n if (args.length === 0) {\n try {\n // Step 1: URL\n url = await input({\n message: 'Notion URL',\n validate: (value) => {\n if (!value.trim()) return 'URL is required';\n if (!value.includes('notion.so') && !value.includes('notion.site')) {\n return 'Please enter a valid Notion URL';\n }\n return true;\n },\n });\n\n // Step 2: Output directory\n const outputDir = await input({\n message: 'Output directory [default: ./nconv-output]',\n default: './nconv-output',\n });\n options.output = outputDir;\n\n // Step 3: Filename (optional, auto-generated if empty)\n const filename = await input({\n message: 'Filename [leave empty for auto-generated]',\n default: '',\n });\n if (filename.trim()) {\n options.filename = filename;\n }\n\n // Step 4: Verbose logging\n options.verbose = await confirm({\n message: 'Enable verbose logging?',\n default: false,\n });\n } catch (error) {\n if (error instanceof Error && error.message === 'User force closed the prompt') {\n logger.warn('\\nConversion cancelled.');\n }\n return;\n }\n } else {\n // CLI mode - parse arguments\n url = args[0];\n const additionalArgs = args.slice(1);\n\n // Parse options\n for (let i = 0; i < additionalArgs.length; i++) {\n const arg = additionalArgs[i];\n if ((arg === '-o' || arg === '--output') && i + 1 < additionalArgs.length) {\n options.output = additionalArgs[++i];\n } else if ((arg === '-i' || arg === '--image-dir') && i + 1 < additionalArgs.length) {\n options.imageDir = additionalArgs[++i];\n } else if ((arg === '-f' || arg === '--filename') && i + 1 < additionalArgs.length) {\n options.filename = additionalArgs[++i];\n } else if (arg === '-v' || arg === '--verbose') {\n options.verbose = true;\n }\n }\n }\n\n // Check if config is valid\n const configCheck = validateConfig();\n if (!configCheck.valid) {\n logger.error('Cannot convert Notion page:');\n logger.error(configCheck.message || 'Configuration is invalid.');\n return;\n }\n\n await mdCommand(url, options);\n}\n\n/**\n * Handle /html command - Convert Notion page to HTML\n */\nexport async function handleHtml(args: string[]): Promise<void> {\n let url = '';\n const options: any = {\n output: './nconv-output',\n imageDir: 'images',\n verbose: false,\n };\n\n // Interactive mode (TUI) - step by step prompts\n if (args.length === 0) {\n try {\n // Step 1: URL\n url = await input({\n message: 'Notion URL',\n validate: (value) => {\n if (!value.trim()) return 'URL is required';\n if (!value.includes('notion.so') && !value.includes('notion.site')) {\n return 'Please enter a valid Notion URL';\n }\n return true;\n },\n });\n\n // Step 2: Output directory\n const outputDir = await input({\n message: 'Output directory [default: ./nconv-output]',\n default: './nconv-output',\n });\n options.output = outputDir;\n\n // Step 3: Filename (optional, auto-generated if empty)\n const filename = await input({\n message: 'Filename [leave empty for auto-generated]',\n default: '',\n });\n if (filename.trim()) {\n options.filename = filename;\n }\n\n // Step 4: Verbose logging\n options.verbose = await confirm({\n message: 'Enable verbose logging?',\n default: false,\n });\n } catch (error) {\n if (error instanceof Error && error.message === 'User force closed the prompt') {\n logger.warn('\\nConversion cancelled.');\n }\n return;\n }\n } else {\n // CLI mode - parse arguments\n url = args[0];\n const additionalArgs = args.slice(1);\n\n // Parse options\n for (let i = 0; i < additionalArgs.length; i++) {\n const arg = additionalArgs[i];\n if ((arg === '-o' || arg === '--output') && i + 1 < additionalArgs.length) {\n options.output = additionalArgs[++i];\n } else if ((arg === '-i' || arg === '--image-dir') && i + 1 < additionalArgs.length) {\n options.imageDir = additionalArgs[++i];\n } else if ((arg === '-f' || arg === '--filename') && i + 1 < additionalArgs.length) {\n options.filename = additionalArgs[++i];\n } else if (arg === '-v' || arg === '--verbose') {\n options.verbose = true;\n }\n }\n }\n\n // Check if config is valid\n const configCheck = validateConfig();\n if (!configCheck.valid) {\n logger.error('Cannot convert Notion page:');\n logger.error(configCheck.message || 'Configuration is invalid.');\n return;\n }\n\n await htmlCommand(url, options);\n}\n\n/**\n * Handle /pdf command - Convert Notion page to PDF\n */\nexport async function handlePdf(args: string[]): Promise<void> {\n let url = '';\n const options: any = {\n output: './nconv-output',\n imageDir: 'images',\n verbose: false,\n };\n\n // Interactive mode (TUI) - step by step prompts\n if (args.length === 0) {\n try {\n // Step 1: URL\n url = await input({\n message: 'Notion URL',\n validate: (value) => {\n if (!value.trim()) return 'URL is required';\n if (!value.includes('notion.so') && !value.includes('notion.site')) {\n return 'Please enter a valid Notion URL';\n }\n return true;\n },\n });\n\n // Step 2: Output directory\n const outputDir = await input({\n message: 'Output directory [default: ./nconv-output]',\n default: './nconv-output',\n });\n options.output = outputDir;\n\n // Step 3: Filename (optional, auto-generated if empty)\n const filename = await input({\n message: 'Filename [leave empty for auto-generated]',\n default: '',\n });\n if (filename.trim()) {\n options.filename = filename;\n }\n\n // Step 4: Verbose logging\n options.verbose = await confirm({\n message: 'Enable verbose logging?',\n default: false,\n });\n } catch (error) {\n if (error instanceof Error && error.message === 'User force closed the prompt') {\n logger.warn('\\nConversion cancelled.');\n }\n return;\n }\n } else {\n // CLI mode - parse arguments\n url = args[0];\n const additionalArgs = args.slice(1);\n\n // Parse options\n for (let i = 0; i < additionalArgs.length; i++) {\n const arg = additionalArgs[i];\n if ((arg === '-o' || arg === '--output') && i + 1 < additionalArgs.length) {\n options.output = additionalArgs[++i];\n } else if ((arg === '-f' || arg === '--filename') && i + 1 < additionalArgs.length) {\n options.filename = additionalArgs[++i];\n } else if (arg === '-v' || arg === '--verbose') {\n options.verbose = true;\n }\n }\n }\n\n // Check if config is valid\n const configCheck = validateConfig();\n if (!configCheck.valid) {\n logger.error('Cannot convert Notion page:');\n logger.error(configCheck.message || 'Configuration is invalid.');\n return;\n }\n\n await pdfCommand(url, options);\n}\n\n/**\n * Available commands list\n */\nconst AVAILABLE_COMMANDS = [\n {\n name: 'init',\n description: 'Initialize configuration (set Notion tokens)',\n examples: ['/init'],\n },\n {\n name: 'config',\n description: 'View and edit current configuration',\n examples: ['/config'],\n },\n {\n name: 'md',\n description: 'Convert Notion page to markdown',\n examples: [\n '/md https://notion.so/page-id',\n '/md https://notion.so/page-id -o ./blog',\n '/md https://notion.so/page-id -o ./blog -f my-post -v',\n ],\n },\n {\n name: 'html',\n description: 'Convert Notion page to HTML',\n examples: [\n '/html https://notion.so/page-id',\n '/html https://notion.so/page-id -o ./blog',\n '/html https://notion.so/page-id -o ./blog -f my-post -v',\n ],\n },\n {\n name: 'pdf',\n description: 'Convert Notion page to PDF (renders actual page)',\n examples: [\n '/pdf https://notion.so/page-id',\n '/pdf https://notion.so/page-id -o ./blog',\n '/pdf https://notion.so/page-id -o ./blog -f my-post -v',\n ],\n },\n {\n name: 'help',\n description: 'Show this help message',\n examples: ['/help'],\n },\n {\n name: 'exit',\n description: 'Exit the REPL',\n examples: ['/exit'],\n },\n];\n\n/**\n * Get command suggestions based on input\n */\nexport function getCommandSuggestions(input: string): string[] {\n const cleanInput = input.toLowerCase().replace(/^\\//, '');\n return AVAILABLE_COMMANDS\n .filter((cmd) => cmd.name.startsWith(cleanInput))\n .map((cmd) => `/${cmd.name}`);\n}\n\n/**\n * Get command choices for autocomplete\n */\nexport function getCommandChoices() {\n return AVAILABLE_COMMANDS.map((cmd) => ({\n title: `/${cmd.name}`,\n value: `/${cmd.name}`,\n description: cmd.description,\n }));\n}\n\n/**\n * Find similar commands using simple string distance\n */\nfunction findSimilarCommand(input: string): string | null {\n const cleanInput = input.toLowerCase();\n for (const cmd of AVAILABLE_COMMANDS) {\n if (cmd.name.includes(cleanInput) || cleanInput.includes(cmd.name)) {\n return cmd.name;\n }\n }\n return null;\n}\n\n/**\n * Handle /help command - Show help information\n */\nexport function handleHelp(): void {\n logger.info('Available commands:\\n');\n\n AVAILABLE_COMMANDS.forEach((cmd) => {\n logger.info(` /${cmd.name.padEnd(20)} ${cmd.description}`);\n });\n\n console.log('');\n logger.info('Conversion options (for /md, /html, and /pdf):');\n logger.info(' -o, --output <dir> Output directory (default: ./nconv-output)');\n logger.info(' -i, --image-dir <dir> Image folder name (default: images) [md/html only]');\n logger.info(' -f, --filename <name> Output filename');\n logger.info(' -v, --verbose Enable verbose logging\\n');\n\n logger.info('Examples:');\n AVAILABLE_COMMANDS.forEach((cmd) => {\n cmd.examples.forEach((example) => {\n logger.info(` ${example}`);\n });\n });\n console.log('');\n}\n\n/**\n * Parse and execute a slash command\n */\nexport async function executeCommand(input: string): Promise<boolean> {\n const trimmed = input.trim();\n\n if (!trimmed) {\n return false;\n }\n\n if (!trimmed.startsWith('/')) {\n logger.error('Commands must start with /');\n logger.info('Type /help for available commands');\n logger.info('Example: /init, /md <url>, /config\\n');\n return false;\n }\n\n const parts = trimmed.slice(1).split(/\\s+/);\n const command = parts[0].toLowerCase();\n const args = parts.slice(1);\n\n switch (command) {\n case 'init':\n await handleInit();\n break;\n\n case 'config':\n await handleConfig();\n break;\n\n case 'md':\n await handleMd(args);\n break;\n\n case 'html':\n await handleHtml(args);\n break;\n\n case 'pdf':\n await handlePdf(args);\n break;\n\n case 'help':\n case 'h':\n handleHelp();\n break;\n\n case 'exit':\n case 'quit':\n case 'q':\n return true;\n\n default:\n logger.error(`Unknown command: /${command}`);\n\n // Try to suggest a similar command\n const similar = findSimilarCommand(command);\n if (similar) {\n logger.info(`Did you mean /${similar}?`);\n }\n\n logger.info('Type /help to see all available commands\\n');\n\n // Show available commands\n const suggestions = getCommandSuggestions(command);\n if (suggestions.length > 0 && suggestions.length < 5) {\n logger.info('Available commands:');\n suggestions.forEach((cmd) => logger.info(` ${cmd}`));\n console.log('');\n }\n }\n\n return false;\n}\n","import { input, confirm } from '@inquirer/prompts';\nimport fs from 'fs';\nimport path from 'path';\nimport os from 'os';\nimport * as logger from '../utils/logger.js';\n\nexport interface TokenConfig {\n TOKEN_V2: string;\n FILE_TOKEN: string;\n}\n\nfunction getConfigPath(): string {\n return path.join(os.homedir(), '.nconv', '.env');\n}\n\nfunction getConfigDir(): string {\n return path.join(os.homedir(), '.nconv');\n}\n\n/**\n * Load existing config from file\n */\nexport function loadConfig(): TokenConfig | null {\n const configPath = getConfigPath();\n if (!fs.existsSync(configPath)) {\n return null;\n }\n\n const content = fs.readFileSync(configPath, 'utf-8');\n const config: TokenConfig = { TOKEN_V2: '', FILE_TOKEN: '' };\n\n content.split('\\n').forEach((line) => {\n const trimmed = line.trim();\n if (trimmed.startsWith('#') || !trimmed) return;\n\n const [key, ...valueParts] = trimmed.split('=');\n const value = valueParts.join('=').trim();\n\n if (key === 'TOKEN_V2') config.TOKEN_V2 = value;\n if (key === 'FILE_TOKEN') config.FILE_TOKEN = value;\n });\n\n return config;\n}\n\n/**\n * Save config to file\n */\nexport function saveConfig(config: TokenConfig): void {\n const configDir = getConfigDir();\n const configPath = getConfigPath();\n\n const envContent = `# Notion Access Tokens\n# These tokens are required to fetch content from Notion.\n# You can find them in your browser's cookies when logged into Notion.\n\nTOKEN_V2=${config.TOKEN_V2}\nFILE_TOKEN=${config.FILE_TOKEN}\n`;\n\n try {\n if (!fs.existsSync(configDir)) {\n fs.mkdirSync(configDir, { recursive: true });\n }\n fs.writeFileSync(configPath, envContent);\n logger.info('✅ Configuration saved successfully.');\n } catch (error) {\n logger.error('Failed to save configuration.');\n if (error instanceof Error) {\n logger.error(error.message);\n }\n throw error;\n }\n}\n\n/**\n * Prompt user to input tokens (for /init)\n */\nexport async function promptInitConfig(): Promise<TokenConfig> {\n logger.info('Please enter your Notion access tokens.');\n logger.info('You can find these in your browser cookies when logged into Notion.\\n');\n\n const TOKEN_V2 = await input({\n message: 'TOKEN_V2:',\n required: true,\n validate: (value) => {\n if (!value.trim()) return 'TOKEN_V2 is required';\n return true;\n },\n });\n\n const FILE_TOKEN = await input({\n message: 'FILE_TOKEN:',\n required: true,\n validate: (value) => {\n if (!value.trim()) return 'FILE_TOKEN is required';\n return true;\n },\n });\n\n return { TOKEN_V2, FILE_TOKEN };\n}\n\n/**\n * Prompt user to edit existing config (for /config)\n */\nexport async function promptEditConfig(existing: TokenConfig): Promise<TokenConfig> {\n logger.info('Current configuration:\\n');\n\n const TOKEN_V2 = await input({\n message: 'TOKEN_V2:',\n default: existing.TOKEN_V2,\n required: true,\n });\n\n const FILE_TOKEN = await input({\n message: 'FILE_TOKEN:',\n default: existing.FILE_TOKEN,\n required: true,\n });\n\n return { TOKEN_V2, FILE_TOKEN };\n}\n"],"mappings":"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","names":["error","error","fs","path","path","fs","os","spinner","error","path","fs","os","spinner","error","path","fs","os","spinner","error","fs","os","path","error","fs","path","os","path","os","fs","error","chalk","input","confirm","fs","path","os","getConfigPath","path","os","fs","error","confirm","error","input","chalk","input","error"]}
|
package/nconv-cli-1.0.0.tgz
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nconv-cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "A CLI tool that
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "A CLI tool that automatically convert Notion pages into Markdown, HTML, and PDF.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -10,25 +10,38 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsup",
|
|
12
12
|
"dev": "tsup --watch",
|
|
13
|
-
"start": "node dist/index.js"
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"postinstall": "patch-package"
|
|
14
15
|
},
|
|
15
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"notion",
|
|
18
|
+
"markdown",
|
|
19
|
+
"blog",
|
|
20
|
+
"converter",
|
|
21
|
+
"cli"
|
|
22
|
+
],
|
|
16
23
|
"author": "",
|
|
17
24
|
"license": "ISC",
|
|
18
25
|
"dependencies": {
|
|
19
|
-
"
|
|
20
|
-
"commander": "^12.0.0",
|
|
26
|
+
"@inquirer/prompts": "^8.2.0",
|
|
21
27
|
"axios": "^1.13.2",
|
|
28
|
+
"chalk": "^5.6.2",
|
|
29
|
+
"commander": "^12.0.0",
|
|
22
30
|
"dotenv": "^17.2.3",
|
|
23
|
-
"
|
|
31
|
+
"md-to-pdf": "^5.2.5",
|
|
32
|
+
"notion-exporter": "^0.8.1",
|
|
24
33
|
"ora": "^8.0.0",
|
|
34
|
+
"patch-package": "^8.0.1",
|
|
35
|
+
"prompts": "^2.4.2",
|
|
25
36
|
"slugify": "^1.6.6",
|
|
26
37
|
"uuid": "^13.0.0"
|
|
27
38
|
},
|
|
28
39
|
"devDependencies": {
|
|
29
|
-
"@types/node": "^25.0
|
|
40
|
+
"@types/node": "^25.1.0",
|
|
41
|
+
"@types/prompts": "^2.4.9",
|
|
30
42
|
"@types/uuid": "^10.0.0",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
43
|
+
"tsup": "^8.0.0",
|
|
44
|
+
"tsx": "^4.21.0",
|
|
45
|
+
"typescript": "^5.9.3"
|
|
33
46
|
}
|
|
34
47
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
diff --git a/node_modules/notion-exporter/dist/index.js b/node_modules/notion-exporter/dist/index.js
|
|
2
|
+
index 26fec15..aa97e8c 100644
|
|
3
|
+
--- a/node_modules/notion-exporter/dist/index.js
|
|
4
|
+
+++ b/node_modules/notion-exporter/dist/index.js
|
|
5
|
+
@@ -111,7 +111,8 @@ var defaultConfig = {
|
|
6
|
+
timeZone: "UTC",
|
|
7
|
+
locale: "en",
|
|
8
|
+
collectionViewExportType: "all",
|
|
9
|
+
- pollInterval: 1e3
|
|
10
|
+
+ pollInterval: 1e3,
|
|
11
|
+
+ exportType: "markdown"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/NotionExporter.ts
|
|
15
|
+
@@ -227,7 +228,7 @@ var NotionExporter = class {
|
|
16
|
+
return __async(this, null, function* () {
|
|
17
|
+
const id = validateUuid(blockIdFromUrl(idOrUrl));
|
|
18
|
+
if (!id) return Promise.reject(`Invalid URL or blockId: ${idOrUrl}`);
|
|
19
|
+
- const _a = this.config, { recursive, pollInterval } = _a, config = __objRest(_a, ["recursive", "pollInterval"]);
|
|
20
|
+
+ const _a = this.config, { recursive, pollInterval, exportType } = _a, config = __objRest(_a, ["recursive", "pollInterval", "exportType"]);
|
|
21
|
+
const res = yield this.client.post("enqueueTask", {
|
|
22
|
+
task: {
|
|
23
|
+
eventName: "exportBlock",
|
|
24
|
+
@@ -237,7 +238,7 @@ var NotionExporter = class {
|
|
25
|
+
recursive: !!recursive,
|
|
26
|
+
shouldExportComments: false,
|
|
27
|
+
exportOptions: __spreadValues({
|
|
28
|
+
- exportType: "markdown"
|
|
29
|
+
+ exportType: exportType || "markdown"
|
|
30
|
+
}, config)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ConverterOptions, createConfig } from '../config.js';
|
|
2
|
+
import { NotionMarkdownExporter } from '../core/exporter.js';
|
|
3
|
+
import { generateSafeFilename, extractTitleFromUrl } from '../utils/file.js';
|
|
4
|
+
import * as logger from '../utils/logger.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* html 명령어 핸들러
|
|
11
|
+
*/
|
|
12
|
+
export async function htmlCommand(notionUrl: string, options: ConverterOptions) {
|
|
13
|
+
const tempDir = path.join(os.tmpdir(), `nconv-cli-${Date.now()}`);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// 설정 생성
|
|
17
|
+
const config = createConfig(options);
|
|
18
|
+
|
|
19
|
+
if (config.verbose) {
|
|
20
|
+
logger.info('Configuration loaded successfully');
|
|
21
|
+
console.log(` Output directory: ${config.output}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 1. Notion에서 HTML과 이미지 가져오기
|
|
25
|
+
const spinner = logger.spinner('Fetching Notion page as HTML...');
|
|
26
|
+
|
|
27
|
+
const exporter = new NotionMarkdownExporter({
|
|
28
|
+
tokenV2: config.tokenV2,
|
|
29
|
+
fileToken: config.fileToken,
|
|
30
|
+
}, 'html');
|
|
31
|
+
|
|
32
|
+
let result;
|
|
33
|
+
try {
|
|
34
|
+
result = await exporter.exportHTML(notionUrl, tempDir);
|
|
35
|
+
spinner.succeed(`Notion page fetched (${result.imageFiles.length} images)`);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
spinner.fail('Failed to fetch Notion page');
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. 파일명/폴더명 생성
|
|
42
|
+
let baseFilename: string;
|
|
43
|
+
if (config.filename) {
|
|
44
|
+
baseFilename = config.filename.replace(/\.html?$/, '');
|
|
45
|
+
} else {
|
|
46
|
+
const title = extractTitleFromUrl(notionUrl);
|
|
47
|
+
baseFilename = generateSafeFilename(title, ''); // 확장자 없이 생성
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. 제목별 폴더 생성 (output/제목/)
|
|
51
|
+
const pageDir = path.join(config.output, baseFilename);
|
|
52
|
+
await fs.mkdir(pageDir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
// 4. 이미지 폴더 생성 및 이미지 파일 이동
|
|
55
|
+
const imageOutputDir = path.join(pageDir, config.imageDir);
|
|
56
|
+
await fs.mkdir(imageOutputDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
if (config.verbose && result.imageFiles.length > 0) {
|
|
59
|
+
console.log(`Processing image files...\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let processedHtml = result.html;
|
|
63
|
+
for (const imageFile of result.imageFiles) {
|
|
64
|
+
try {
|
|
65
|
+
// 파일명에서 공백을 하이픈으로 변경 (호환성)
|
|
66
|
+
const originalFileName = path.basename(imageFile.filename);
|
|
67
|
+
const safeFileName = originalFileName.replace(/\s+/g, '-');
|
|
68
|
+
|
|
69
|
+
// 이미지 파일 복사
|
|
70
|
+
const targetPath = path.join(imageOutputDir, safeFileName);
|
|
71
|
+
await fs.copyFile(imageFile.sourcePath, targetPath);
|
|
72
|
+
|
|
73
|
+
if (config.verbose) {
|
|
74
|
+
console.log(`✓ ${safeFileName}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// HTML 내 경로를 상대경로로 변경
|
|
78
|
+
const originalPath = imageFile.filename;
|
|
79
|
+
const relativePath = `./${config.imageDir}/${safeFileName}`;
|
|
80
|
+
|
|
81
|
+
// Notion이 URL 인코딩하는 방식: 각 경로 부분을 개별적으로 인코딩
|
|
82
|
+
const pathParts = originalPath.split('/');
|
|
83
|
+
const encodedPath = pathParts.map(part => encodeURIComponent(part)).join('/');
|
|
84
|
+
|
|
85
|
+
// 정규식 특수문자 이스케이프 함수
|
|
86
|
+
const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
|
|
88
|
+
// HTML에서 모든 가능한 형태의 경로를 교체
|
|
89
|
+
// src="..." 형태와 href="..." 형태 모두 처리
|
|
90
|
+
processedHtml = processedHtml
|
|
91
|
+
.replace(new RegExp(`(src|href)="${escapeRegex(originalPath)}"`, 'g'), `$1="${relativePath}"`)
|
|
92
|
+
.replace(new RegExp(`(src|href)="${escapeRegex(encodedPath)}"`, 'g'), `$1="${relativePath}"`);
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (config.verbose) {
|
|
96
|
+
const errorMsg = error instanceof Error ? error.message : '알 수 없는 오류';
|
|
97
|
+
console.error(`✗ ${imageFile.filename}: ${errorMsg}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 5. HTML 파일 저장 (제목 폴더 안에)
|
|
103
|
+
const filename = `${baseFilename}.html`;
|
|
104
|
+
const filePath = path.join(pageDir, filename);
|
|
105
|
+
await fs.writeFile(filePath, processedHtml, 'utf-8');
|
|
106
|
+
|
|
107
|
+
// 6. 결과 출력
|
|
108
|
+
console.log('');
|
|
109
|
+
logger.success('Conversion complete!');
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(`📁 Folder: ${path.relative(process.cwd(), pageDir)}`);
|
|
112
|
+
console.log(`📄 HTML: ${filename}`);
|
|
113
|
+
|
|
114
|
+
if (result.imageFiles.length > 0) {
|
|
115
|
+
console.log(`🖼️ Images: ${config.imageDir}/ (${result.imageFiles.length} files)`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('');
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof Error) {
|
|
122
|
+
logger.error(error.message);
|
|
123
|
+
} else {
|
|
124
|
+
logger.error('An unknown error occurred.');
|
|
125
|
+
}
|
|
126
|
+
process.exit(1);
|
|
127
|
+
} finally {
|
|
128
|
+
// 임시 디렉토리 정리
|
|
129
|
+
try {
|
|
130
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
131
|
+
} catch {
|
|
132
|
+
// 무시
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Arguments, CommandBuilder } from 'yargs';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import * as logger from '../utils/logger.js';
|
|
7
|
+
|
|
8
|
+
type Options = {
|
|
9
|
+
// No options yet
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const command: string = 'init';
|
|
13
|
+
export const desc: string = 'Create a default .env configuration file in your home directory.';
|
|
14
|
+
|
|
15
|
+
export const builder: CommandBuilder<Options, Options> = (yargs) =>
|
|
16
|
+
yargs
|
|
17
|
+
.example([
|
|
18
|
+
['$0 init', 'Create the default configuration file.'],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const handler = async (argv: Arguments<Options>): Promise<void> => {
|
|
22
|
+
const configDir = path.join(os.homedir(), '.nconv');
|
|
23
|
+
const configFile = path.join(configDir, '.env');
|
|
24
|
+
|
|
25
|
+
logger.info(`Checking for config file at: ${configFile}`);
|
|
26
|
+
|
|
27
|
+
if (fs.existsSync(configFile)) {
|
|
28
|
+
logger.warn('Configuration file already exists.');
|
|
29
|
+
logger.warn(`If you want to re-initialize, please delete the file first: ${configFile}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const envContent = `
|
|
34
|
+
# Please provide your Notion access tokens.
|
|
35
|
+
# These are required to fetch content from your Notion pages.
|
|
36
|
+
# You can find these tokens in your browser's cookies when you are logged into Notion.
|
|
37
|
+
TOKEN_V2=
|
|
38
|
+
FILE_TOKEN=
|
|
39
|
+
`.trim();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
logger.info(`Creating directory at: ${configDir}`);
|
|
43
|
+
if (!fs.existsSync(configDir)) {
|
|
44
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
fs.writeFileSync(configFile, envContent);
|
|
47
|
+
logger.info('✅ Successfully created configuration file.');
|
|
48
|
+
logger.info(`Please edit the file to set your environment variables: ${configFile}`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.error('Failed to create configuration file.');
|
|
51
|
+
if (error instanceof Error) {
|
|
52
|
+
logger.error(error.message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
package/src/commands/md.ts
CHANGED
|
@@ -51,10 +51,6 @@ export async function mdCommand(notionUrl: string, options: ConverterOptions) {
|
|
|
51
51
|
const pageDir = path.join(config.output, baseFilename);
|
|
52
52
|
await fs.mkdir(pageDir, { recursive: true });
|
|
53
53
|
|
|
54
|
-
if (config.verbose) {
|
|
55
|
-
console.log(`📁 출력 폴더: ${path.relative(process.cwd(), pageDir)}\n`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
54
|
// 4. 이미지 폴더 생성 및 이미지 파일 이동
|
|
59
55
|
const imageOutputDir = path.join(pageDir, config.imageDir);
|
|
60
56
|
await fs.mkdir(imageOutputDir, { recursive: true });
|