markdown-to-document-cli 1.1.4 → 1.2.8
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/README.md +248 -18
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +523 -134
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/services/CoverService.d.ts +25 -3
- package/dist/services/CoverService.d.ts.map +1 -1
- package/dist/services/CoverService.js +226 -97
- package/dist/services/CoverService.js.map +1 -1
- package/dist/services/MarkdownPreprocessor.d.ts +5 -0
- package/dist/services/MarkdownPreprocessor.d.ts.map +1 -1
- package/dist/services/MarkdownPreprocessor.js +18 -7
- package/dist/services/MarkdownPreprocessor.js.map +1 -1
- package/dist/services/PandocService.d.ts +7 -1
- package/dist/services/PandocService.d.ts.map +1 -1
- package/dist/services/PandocService.js +61 -15
- package/dist/services/PandocService.js.map +1 -1
- package/dist/services/TypographyService.d.ts +8 -1
- package/dist/services/TypographyService.d.ts.map +1 -1
- package/dist/services/TypographyService.js +417 -57
- package/dist/services/TypographyService.js.map +1 -1
- package/dist/types/index.d.ts +37 -11
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +28 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +224 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/cssBuilder.d.ts +93 -0
- package/dist/utils/cssBuilder.d.ts.map +1 -0
- package/dist/utils/cssBuilder.js +231 -0
- package/dist/utils/cssBuilder.js.map +1 -0
- package/dist/utils/dependencyChecker.d.ts +63 -0
- package/dist/utils/dependencyChecker.d.ts.map +1 -0
- package/dist/utils/dependencyChecker.js +268 -0
- package/dist/utils/dependencyChecker.js.map +1 -0
- package/dist/utils/pathValidator.d.ts +48 -0
- package/dist/utils/pathValidator.d.ts.map +1 -0
- package/dist/utils/pathValidator.js +188 -0
- package/dist/utils/pathValidator.js.map +1 -0
- package/dist/utils/themeUtils.d.ts +70 -0
- package/dist/utils/themeUtils.d.ts.map +1 -0
- package/dist/utils/themeUtils.js +141 -0
- package/dist/utils/themeUtils.js.map +1 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Markdown to Document CLI
|
|
4
4
|
*
|
|
5
|
+
* Refactored for improved UX with streamlined Interactive Mode
|
|
6
|
+
*
|
|
5
7
|
* Usage:
|
|
6
8
|
* npx markdown-to-document-cli <input.md>
|
|
7
9
|
* m2d <input.md> [options]
|
|
10
|
+
* m2d interactive (or m2d i)
|
|
8
11
|
*/
|
|
9
12
|
import { Command } from 'commander';
|
|
10
13
|
import chalk from 'chalk';
|
|
@@ -13,25 +16,241 @@ import inquirer from 'inquirer';
|
|
|
13
16
|
import { MarkdownToDocument } from './index.js';
|
|
14
17
|
import { TYPOGRAPHY_PRESETS, COVER_THEMES } from './utils/constants.js';
|
|
15
18
|
import { Logger } from './utils/common.js';
|
|
19
|
+
import { DependencyChecker } from './utils/dependencyChecker.js';
|
|
20
|
+
import { PathValidator } from './utils/pathValidator.js';
|
|
16
21
|
import * as path from 'path';
|
|
17
22
|
import * as fs from 'fs';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
18
24
|
const program = new Command();
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = path.dirname(__filename);
|
|
27
|
+
const getCliVersion = () => {
|
|
28
|
+
try {
|
|
29
|
+
const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
|
|
30
|
+
const raw = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
return parsed.version || '0.0.0';
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return '0.0.0';
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Analyze markdown content for Obsidian syntax and output optimization needs
|
|
40
|
+
*/
|
|
41
|
+
function analyzeMarkdownContent(content) {
|
|
42
|
+
const result = {
|
|
43
|
+
hasObsidianImages: false,
|
|
44
|
+
hasObsidianLinks: false,
|
|
45
|
+
hasHighlights: false,
|
|
46
|
+
hasCallouts: false,
|
|
47
|
+
hasLongCodeLines: false,
|
|
48
|
+
hasComplexTables: false,
|
|
49
|
+
hasMultipleH1: false,
|
|
50
|
+
hasFrontmatter: false,
|
|
51
|
+
imageCount: 0,
|
|
52
|
+
tableCount: 0,
|
|
53
|
+
codeBlockCount: 0,
|
|
54
|
+
wordCount: 0,
|
|
55
|
+
recommendPreprocess: false,
|
|
56
|
+
recommendedPreset: 'ebook',
|
|
57
|
+
issues: [],
|
|
58
|
+
};
|
|
59
|
+
// Check for YAML frontmatter
|
|
60
|
+
result.hasFrontmatter = /^---\n[\s\S]*?\n---/.test(content);
|
|
61
|
+
// Check for Obsidian image syntax: ![[image]]
|
|
62
|
+
const obsidianImageMatches = content.match(/!\[\[([^\]]+)\]\]/g);
|
|
63
|
+
result.hasObsidianImages = !!obsidianImageMatches;
|
|
64
|
+
if (obsidianImageMatches) {
|
|
65
|
+
result.issues.push(`Obsidian 이미지 문법 ${obsidianImageMatches.length}개 발견`);
|
|
66
|
+
}
|
|
67
|
+
// Check for Obsidian internal links: [[link]]
|
|
68
|
+
const obsidianLinkMatches = content.match(/(?<!!)\[\[([^\]]+)\]\]/g);
|
|
69
|
+
result.hasObsidianLinks = !!obsidianLinkMatches;
|
|
70
|
+
if (obsidianLinkMatches) {
|
|
71
|
+
result.issues.push(`Obsidian 내부 링크 ${obsidianLinkMatches.length}개 발견`);
|
|
72
|
+
}
|
|
73
|
+
// Check for highlights: ==text==
|
|
74
|
+
const highlightMatches = content.match(/==([^=]+)==/g);
|
|
75
|
+
result.hasHighlights = !!highlightMatches;
|
|
76
|
+
if (highlightMatches) {
|
|
77
|
+
result.issues.push(`하이라이트 문법 ${highlightMatches.length}개 발견`);
|
|
78
|
+
}
|
|
79
|
+
// Check for callouts: > [!type]
|
|
80
|
+
const calloutMatches = content.match(/>\s*\[!(\w+)\]/g);
|
|
81
|
+
result.hasCallouts = !!calloutMatches;
|
|
82
|
+
if (calloutMatches) {
|
|
83
|
+
result.issues.push(`콜아웃 ${calloutMatches.length}개 발견`);
|
|
84
|
+
}
|
|
85
|
+
// Count images (standard markdown)
|
|
86
|
+
const standardImageMatches = content.match(/!\[([^\]]*)\]\([^)]+\)/g);
|
|
87
|
+
result.imageCount = (obsidianImageMatches?.length || 0) + (standardImageMatches?.length || 0);
|
|
88
|
+
// Count tables
|
|
89
|
+
const tableMatches = content.match(/\|.*\|.*\n\|[-:| ]+\|/g);
|
|
90
|
+
result.tableCount = tableMatches?.length || 0;
|
|
91
|
+
// Check for complex tables (>5 columns or very long cells)
|
|
92
|
+
if (tableMatches) {
|
|
93
|
+
for (const table of tableMatches) {
|
|
94
|
+
const columns = (table.match(/\|/g)?.length || 0) - 1;
|
|
95
|
+
if (columns > 5) {
|
|
96
|
+
result.hasComplexTables = true;
|
|
97
|
+
result.issues.push('5열 초과 복잡한 표 발견');
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Count code blocks and check for long lines
|
|
103
|
+
const codeBlockMatches = content.match(/```[\s\S]*?```/g);
|
|
104
|
+
result.codeBlockCount = codeBlockMatches?.length || 0;
|
|
105
|
+
if (codeBlockMatches) {
|
|
106
|
+
for (const block of codeBlockMatches) {
|
|
107
|
+
const lines = block.split('\n');
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
if (line.length > 100) {
|
|
110
|
+
result.hasLongCodeLines = true;
|
|
111
|
+
result.issues.push('100자 초과 코드 라인 발견 (PDF 잘림 위험)');
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (result.hasLongCodeLines)
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Check for multiple H1
|
|
120
|
+
const h1Matches = content.match(/^#\s+[^\n]+/gm);
|
|
121
|
+
result.hasMultipleH1 = (h1Matches?.length || 0) > 1;
|
|
122
|
+
if (result.hasMultipleH1) {
|
|
123
|
+
result.issues.push(`H1 제목 ${h1Matches?.length}개 발견 (1개 권장)`);
|
|
124
|
+
}
|
|
125
|
+
// Word count (rough estimate)
|
|
126
|
+
const textOnly = content.replace(/```[\s\S]*?```/g, '').replace(/[#*`\[\]()]/g, '');
|
|
127
|
+
result.wordCount = textOnly.split(/\s+/).filter(w => w.length > 0).length;
|
|
128
|
+
// Determine if preprocessing is recommended
|
|
129
|
+
result.recommendPreprocess =
|
|
130
|
+
result.hasObsidianImages ||
|
|
131
|
+
result.hasObsidianLinks ||
|
|
132
|
+
result.hasHighlights ||
|
|
133
|
+
result.hasCallouts ||
|
|
134
|
+
result.hasLongCodeLines ||
|
|
135
|
+
result.hasComplexTables ||
|
|
136
|
+
result.hasMultipleH1;
|
|
137
|
+
// Recommend typography preset based on content analysis
|
|
138
|
+
if (result.imageCount > 10) {
|
|
139
|
+
result.recommendedPreset = 'image_heavy';
|
|
140
|
+
}
|
|
141
|
+
else if (result.tableCount > 5) {
|
|
142
|
+
result.recommendedPreset = 'table_heavy';
|
|
143
|
+
}
|
|
144
|
+
else if (result.codeBlockCount > 10) {
|
|
145
|
+
result.recommendedPreset = 'manual';
|
|
146
|
+
}
|
|
147
|
+
else if (result.wordCount > 10000) {
|
|
148
|
+
result.recommendedPreset = 'text_heavy';
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
result.recommendedPreset = 'balanced';
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Display analysis result to console
|
|
157
|
+
*/
|
|
158
|
+
function displayAnalysisResult(result) {
|
|
159
|
+
console.log(chalk.bold('📊 문서 분석 결과:\n'));
|
|
160
|
+
// Statistics
|
|
161
|
+
console.log(chalk.gray(' 📝 단어 수:'), chalk.cyan(`약 ${result.wordCount.toLocaleString()}개`));
|
|
162
|
+
console.log(chalk.gray(' 🖼️ 이미지:'), chalk.cyan(`${result.imageCount}개`));
|
|
163
|
+
console.log(chalk.gray(' 📊 표:'), chalk.cyan(`${result.tableCount}개`));
|
|
164
|
+
console.log(chalk.gray(' 💻 코드 블록:'), chalk.cyan(`${result.codeBlockCount}개`));
|
|
165
|
+
console.log(chalk.gray(' 📋 Frontmatter:'), result.hasFrontmatter ? chalk.green('있음') : chalk.yellow('없음'));
|
|
166
|
+
// Issues found
|
|
167
|
+
if (result.issues.length > 0) {
|
|
168
|
+
console.log(chalk.yellow('\n⚠️ 발견된 이슈:'));
|
|
169
|
+
result.issues.forEach(issue => {
|
|
170
|
+
console.log(chalk.yellow(` • ${issue}`));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
console.log(chalk.green('\n✅ 특별한 이슈 없음 - 표준 Markdown'));
|
|
175
|
+
}
|
|
176
|
+
// Recommendation
|
|
177
|
+
console.log(chalk.bold('\n💡 권장 사항:'));
|
|
178
|
+
if (result.recommendPreprocess) {
|
|
179
|
+
console.log(chalk.green(' → 문서 최적화가 필요하지만, 변환 과정에서 자동으로 적용됩니다.'));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.log(chalk.blue(' → 바로 변환해도 안정적입니다.'));
|
|
183
|
+
}
|
|
184
|
+
console.log(chalk.gray(` → 추천 프리셋: ${result.recommendedPreset}`));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get typography preset choices with recommended preset highlighted
|
|
188
|
+
*/
|
|
189
|
+
function getTypographyPresetChoices(analysisResult) {
|
|
190
|
+
const presetCategories = {
|
|
191
|
+
'Basic': ['novel', 'presentation', 'review', 'ebook'],
|
|
192
|
+
'Content-focused': ['text_heavy', 'table_heavy', 'image_heavy', 'balanced'],
|
|
193
|
+
'Document Type': ['report', 'manual', 'magazine'],
|
|
194
|
+
};
|
|
195
|
+
const choices = [];
|
|
196
|
+
for (const [category, presetIds] of Object.entries(presetCategories)) {
|
|
197
|
+
choices.push(new inquirer.Separator(chalk.bold(`\n── ${category} ──`)));
|
|
198
|
+
for (const presetId of presetIds) {
|
|
199
|
+
const preset = TYPOGRAPHY_PRESETS[presetId];
|
|
200
|
+
if (preset) {
|
|
201
|
+
const isRecommended = presetId === analysisResult.recommendedPreset;
|
|
202
|
+
const name = isRecommended
|
|
203
|
+
? chalk.green(`★ ${preset.name}`) + chalk.gray(` - ${preset.description}`) + chalk.green(' (권장)')
|
|
204
|
+
: chalk.cyan(preset.name) + chalk.gray(` - ${preset.description}`);
|
|
205
|
+
choices.push({ name, value: presetId });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return choices;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get cover theme choices grouped by category
|
|
213
|
+
*/
|
|
214
|
+
function getCoverThemeChoices() {
|
|
215
|
+
const themeCategories = {
|
|
216
|
+
'Basic': ['apple', 'modern_gradient', 'dark_tech', 'nature', 'classic_book', 'minimalist'],
|
|
217
|
+
'Professional': ['corporate', 'academic', 'magazine'],
|
|
218
|
+
'Creative': ['sunset', 'ocean', 'aurora', 'rose_gold'],
|
|
219
|
+
'Seasonal': ['spring', 'autumn', 'winter'],
|
|
220
|
+
};
|
|
221
|
+
const choices = [];
|
|
222
|
+
for (const [category, themeIds] of Object.entries(themeCategories)) {
|
|
223
|
+
choices.push(new inquirer.Separator(chalk.bold(`\n── ${category} ──`)));
|
|
224
|
+
for (const themeId of themeIds) {
|
|
225
|
+
const theme = COVER_THEMES[themeId];
|
|
226
|
+
if (theme) {
|
|
227
|
+
choices.push({
|
|
228
|
+
name: chalk.cyan(theme.name) + chalk.gray(` - ${theme.description}`),
|
|
229
|
+
value: themeId,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return choices;
|
|
235
|
+
}
|
|
19
236
|
// Configure CLI
|
|
20
237
|
program
|
|
21
238
|
.name('markdown-to-document')
|
|
22
239
|
.alias('m2d')
|
|
23
240
|
.description('Professional-grade EPUB/PDF conversion tool for Markdown files')
|
|
24
|
-
.version(
|
|
241
|
+
.version(getCliVersion())
|
|
25
242
|
.argument('<input>', 'Input markdown file path')
|
|
243
|
+
.option('--title <title>', 'Book title (defaults to frontmatter title or filename)')
|
|
244
|
+
.option('--author <author>', 'Author name (defaults to frontmatter author)')
|
|
26
245
|
.option('-o, --output <path>', 'Output directory')
|
|
27
|
-
.option('-f, --format <format>', 'Output format (epub, pdf, both)', '
|
|
28
|
-
.option('-t, --typography <preset>', 'Typography preset (novel, presentation, review, ebook)', '
|
|
246
|
+
.option('-f, --format <format>', 'Output format (epub, pdf, both)', 'both')
|
|
247
|
+
.option('-t, --typography <preset>', 'Typography preset (auto, novel, presentation, review, ebook, text_heavy, table_heavy, image_heavy, balanced, report, manual, magazine)', 'auto')
|
|
29
248
|
.option('-c, --cover <theme>', 'Cover theme')
|
|
30
249
|
.option('--no-validate', 'Skip content validation')
|
|
31
250
|
.option('--no-auto-fix', 'Disable auto-fix')
|
|
32
251
|
.option('--toc-depth <number>', 'Table of contents depth', '2')
|
|
33
252
|
.option('--no-toc', 'Disable table of contents')
|
|
34
|
-
.option('--pdf-engine <engine>', 'PDF engine (pdflatex, xelatex, weasyprint)', '
|
|
253
|
+
.option('--pdf-engine <engine>', 'PDF engine (auto, pdflatex, xelatex, weasyprint)', 'auto')
|
|
35
254
|
.option('--paper-size <size>', 'Paper size (a4, letter)', 'a4')
|
|
36
255
|
.option('--font-subsetting', 'Enable font subsetting')
|
|
37
256
|
.option('--css <path>', 'Custom CSS file path')
|
|
@@ -44,35 +263,29 @@ program
|
|
|
44
263
|
Logger.setEnabled(true);
|
|
45
264
|
process.env.DEBUG = 'true';
|
|
46
265
|
}
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
console.error(chalk.red(`❌ Error: Input file not found: ${inputPath}`));
|
|
266
|
+
// Validate and normalize input path
|
|
267
|
+
const pathValidation = PathValidator.validatePath(input);
|
|
268
|
+
if (!pathValidation.valid) {
|
|
269
|
+
PathValidator.displayValidationError(pathValidation);
|
|
52
270
|
process.exit(1);
|
|
53
271
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
272
|
+
const inputPath = pathValidation.normalizedPath;
|
|
273
|
+
const fileContent = fs.readFileSync(inputPath, 'utf-8');
|
|
274
|
+
const analysisResult = analyzeMarkdownContent(fileContent);
|
|
275
|
+
const metadata = extractMetadata(fileContent);
|
|
276
|
+
const inferredTitle = metadata.title || path.basename(inputPath, '.md');
|
|
277
|
+
const inferredAuthor = metadata.author || '';
|
|
278
|
+
const customTitle = (options.title || inferredTitle).trim();
|
|
279
|
+
const customAuthor = (options.author || inferredAuthor).trim();
|
|
280
|
+
const typographyOption = String(options.typography || 'auto');
|
|
281
|
+
const typographyPreset = typographyOption === 'auto' ? analysisResult.recommendedPreset : typographyOption;
|
|
58
282
|
console.log(chalk.cyan.bold('\n📚 Markdown to Document CLI\n'));
|
|
59
|
-
// Initialize converter
|
|
60
|
-
const spinner = ora('Initializing...').start();
|
|
61
|
-
const converter = new MarkdownToDocument(options.pandocPath);
|
|
62
|
-
const initResult = await converter.initialize();
|
|
63
|
-
if (!initResult.success) {
|
|
64
|
-
spinner.fail('Initialization failed');
|
|
65
|
-
console.error(chalk.red(`❌ ${initResult.error}`));
|
|
66
|
-
console.log(chalk.yellow('\n' + MarkdownToDocument.getInstallInstructions()));
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
spinner.succeed('Initialized successfully');
|
|
70
283
|
// Prepare conversion options
|
|
71
284
|
const conversionOptions = {
|
|
72
285
|
inputPath,
|
|
73
286
|
outputPath: options.output ? path.resolve(options.output) : undefined,
|
|
74
287
|
format: options.format,
|
|
75
|
-
typographyPreset:
|
|
288
|
+
typographyPreset: typographyPreset,
|
|
76
289
|
coverTheme: options.cover,
|
|
77
290
|
validateContent: options.validate !== false,
|
|
78
291
|
autoFix: options.autoFix !== false,
|
|
@@ -82,12 +295,36 @@ program
|
|
|
82
295
|
paperSize: options.paperSize,
|
|
83
296
|
enableFontSubsetting: options.fontSubsetting,
|
|
84
297
|
cssPath: options.css ? path.resolve(options.css) : undefined,
|
|
298
|
+
customTitle,
|
|
299
|
+
customAuthor: customAuthor || undefined,
|
|
85
300
|
};
|
|
301
|
+
// Check dependencies proactively
|
|
302
|
+
const depChecker = new DependencyChecker();
|
|
303
|
+
const isReady = await depChecker.quickCheck(conversionOptions.format);
|
|
304
|
+
if (!isReady) {
|
|
305
|
+
await depChecker.displayDependencyReport();
|
|
306
|
+
console.log(chalk.red('\n❌ 필수 의존성을 먼저 설치해 주세요.\n'));
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
// Initialize converter
|
|
310
|
+
const spinner = ora('Initializing...').start();
|
|
311
|
+
const converter = new MarkdownToDocument(options.pandocPath);
|
|
312
|
+
const initResult = await converter.initialize();
|
|
313
|
+
if (!initResult.success) {
|
|
314
|
+
spinner.fail('Initialization failed');
|
|
315
|
+
console.error(chalk.red(`❌ ${initResult.error}`));
|
|
316
|
+
console.log(chalk.yellow('\n' + MarkdownToDocument.getInstallInstructions()));
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
spinner.succeed('Initialized successfully');
|
|
86
320
|
// Show conversion info
|
|
87
321
|
console.log(chalk.gray('─'.repeat(50)));
|
|
88
322
|
console.log(chalk.bold('📄 Input:'), chalk.cyan(inputPath));
|
|
89
323
|
console.log(chalk.bold('📤 Format:'), chalk.cyan(conversionOptions.format.toUpperCase()));
|
|
90
324
|
console.log(chalk.bold('🎨 Typography:'), chalk.cyan(TYPOGRAPHY_PRESETS[conversionOptions.typographyPreset]?.name || conversionOptions.typographyPreset));
|
|
325
|
+
console.log(chalk.bold('📖 Title:'), chalk.cyan(customTitle));
|
|
326
|
+
if (customAuthor)
|
|
327
|
+
console.log(chalk.bold('✍️ Author:'), chalk.cyan(customAuthor));
|
|
91
328
|
if (conversionOptions.coverTheme) {
|
|
92
329
|
console.log(chalk.bold('🖼️ Cover:'), chalk.cyan(COVER_THEMES[conversionOptions.coverTheme]?.name || conversionOptions.coverTheme));
|
|
93
330
|
}
|
|
@@ -154,154 +391,307 @@ program
|
|
|
154
391
|
process.exit(1);
|
|
155
392
|
}
|
|
156
393
|
});
|
|
157
|
-
|
|
394
|
+
/**
|
|
395
|
+
* Extract metadata from frontmatter
|
|
396
|
+
*/
|
|
397
|
+
function extractMetadata(content) {
|
|
398
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
399
|
+
if (!frontmatterMatch)
|
|
400
|
+
return {};
|
|
401
|
+
const frontmatter = frontmatterMatch[1];
|
|
402
|
+
const titleMatch = frontmatter.match(/^title:\s*["']?(.+?)["']?\s*$/m);
|
|
403
|
+
const authorMatch = frontmatter.match(/^author:\s*["']?(.+?)["']?\s*$/m);
|
|
404
|
+
return {
|
|
405
|
+
title: titleMatch?.[1]?.trim(),
|
|
406
|
+
author: authorMatch?.[1]?.trim(),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Get simplified preset choices (top 6 most useful)
|
|
411
|
+
*/
|
|
412
|
+
function getSimplifiedPresetChoices(recommendedPreset) {
|
|
413
|
+
const topPresets = ['ebook', 'novel', 'report', 'presentation', 'table_heavy', 'image_heavy'];
|
|
414
|
+
return topPresets.map(presetId => {
|
|
415
|
+
const preset = TYPOGRAPHY_PRESETS[presetId];
|
|
416
|
+
if (!preset)
|
|
417
|
+
return null;
|
|
418
|
+
const isRecommended = presetId === recommendedPreset;
|
|
419
|
+
const name = isRecommended
|
|
420
|
+
? chalk.green(`★ ${preset.name}`) + chalk.gray(` - ${preset.description}`)
|
|
421
|
+
: chalk.cyan(preset.name) + chalk.gray(` - ${preset.description}`);
|
|
422
|
+
return { name, value: presetId };
|
|
423
|
+
}).filter(Boolean);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Get simplified cover theme choices (top 6)
|
|
427
|
+
*/
|
|
428
|
+
function getSimplifiedThemeChoices() {
|
|
429
|
+
const topThemes = ['apple', 'modern_gradient', 'academic', 'corporate', 'minimalist', 'classic_book'];
|
|
430
|
+
return topThemes.map(themeId => {
|
|
431
|
+
const theme = COVER_THEMES[themeId];
|
|
432
|
+
if (!theme)
|
|
433
|
+
return null;
|
|
434
|
+
return {
|
|
435
|
+
name: chalk.cyan(theme.name) + chalk.gray(` - ${theme.description}`),
|
|
436
|
+
value: themeId,
|
|
437
|
+
};
|
|
438
|
+
}).filter(Boolean);
|
|
439
|
+
}
|
|
440
|
+
// Interactive mode - Refactored for better UX
|
|
158
441
|
program
|
|
159
442
|
.command('interactive')
|
|
160
443
|
.alias('i')
|
|
161
|
-
.description('Interactive mode with
|
|
444
|
+
.description('Interactive mode with streamlined workflow')
|
|
162
445
|
.action(async () => {
|
|
163
|
-
console.log(chalk.cyan.bold('\n' + '
|
|
164
|
-
console.log(chalk.cyan.bold(' Markdown to Document - Interactive Mode'));
|
|
165
|
-
console.log(chalk.cyan.bold('
|
|
166
|
-
|
|
446
|
+
console.log(chalk.cyan.bold('\n' + '═'.repeat(60)));
|
|
447
|
+
console.log(chalk.cyan.bold(' 📚 Markdown to Document - Interactive Mode'));
|
|
448
|
+
console.log(chalk.cyan.bold('═'.repeat(60) + '\n'));
|
|
449
|
+
// ============ STEP 1: 파일 선택 ============
|
|
450
|
+
console.log(chalk.gray(' Step 1/3: 파일 선택\n'));
|
|
451
|
+
const fileAnswer = await inquirer.prompt([
|
|
167
452
|
{
|
|
168
453
|
type: 'input',
|
|
169
454
|
name: 'inputPath',
|
|
170
|
-
message: chalk.yellow('📄
|
|
455
|
+
message: chalk.yellow('📄 마크다운 파일 경로:'),
|
|
171
456
|
validate: (input) => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
457
|
+
const validation = PathValidator.validatePath(input);
|
|
458
|
+
if (!validation.valid) {
|
|
459
|
+
// Return first error and suggestion
|
|
460
|
+
let errorMsg = validation.error || '잘못된 경로입니다.';
|
|
461
|
+
if (validation.suggestions && validation.suggestions.length > 0) {
|
|
462
|
+
errorMsg += '\n' + chalk.gray(' 💡 ' + validation.suggestions[0]);
|
|
463
|
+
}
|
|
464
|
+
return errorMsg;
|
|
177
465
|
}
|
|
178
466
|
return true;
|
|
179
467
|
},
|
|
180
|
-
transformer: (input) =>
|
|
181
|
-
// 입력값 표시 시에도 따옴표 제거
|
|
182
|
-
return input.trim().replace(/^['"]|['"]$/g, '');
|
|
183
|
-
},
|
|
468
|
+
transformer: (input) => PathValidator.normalizePath(input),
|
|
184
469
|
},
|
|
470
|
+
]);
|
|
471
|
+
const pathValidation = PathValidator.validatePath(fileAnswer.inputPath);
|
|
472
|
+
if (!pathValidation.valid || !pathValidation.normalizedPath) {
|
|
473
|
+
PathValidator.displayValidationError(pathValidation);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
const resolvedInputPath = pathValidation.normalizedPath;
|
|
477
|
+
const fileContent = fs.readFileSync(resolvedInputPath, 'utf-8');
|
|
478
|
+
// 문서 분석 (자동)
|
|
479
|
+
const analysisResult = analyzeMarkdownContent(fileContent);
|
|
480
|
+
const metadata = extractMetadata(fileContent);
|
|
481
|
+
// 제목/저자: 반드시 사용자 입력을 받으며, 입력값을 항상 변환에 반영
|
|
482
|
+
const metaAnswers = await inquirer.prompt([
|
|
185
483
|
{
|
|
186
484
|
type: 'input',
|
|
187
485
|
name: 'customTitle',
|
|
188
|
-
message: chalk.yellow('📖
|
|
189
|
-
default: '',
|
|
486
|
+
message: chalk.yellow('📖 책 제목 (Enter=자동):'),
|
|
487
|
+
default: metadata.title || path.basename(resolvedInputPath, '.md'),
|
|
488
|
+
validate: () => true,
|
|
489
|
+
transformer: (input) => input,
|
|
190
490
|
},
|
|
191
491
|
{
|
|
192
492
|
type: 'input',
|
|
193
493
|
name: 'customAuthor',
|
|
194
|
-
message: chalk.yellow('✍️
|
|
195
|
-
default: '',
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
type: 'list',
|
|
199
|
-
name: 'format',
|
|
200
|
-
message: chalk.yellow('📤 Output format:'),
|
|
201
|
-
choices: [
|
|
202
|
-
{ name: chalk.magenta('📚 Both EPUB and PDF'), value: 'both' },
|
|
203
|
-
{ name: chalk.blue('📄 PDF only'), value: 'pdf' },
|
|
204
|
-
{ name: chalk.green('📖 EPUB only'), value: 'epub' },
|
|
205
|
-
],
|
|
206
|
-
default: 'both',
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
type: 'list',
|
|
210
|
-
name: 'typographyPreset',
|
|
211
|
-
message: chalk.yellow('🎨 Typography preset:'),
|
|
212
|
-
choices: Object.values(TYPOGRAPHY_PRESETS).map(preset => ({
|
|
213
|
-
name: `${chalk.cyan(preset.name)} - ${chalk.gray(preset.description)}`,
|
|
214
|
-
value: preset.id,
|
|
215
|
-
})),
|
|
216
|
-
default: 'ebook',
|
|
494
|
+
message: chalk.yellow('✍️ 저자 (Enter=자동):'),
|
|
495
|
+
default: metadata.author || '',
|
|
496
|
+
validate: () => true,
|
|
497
|
+
transformer: (input) => input,
|
|
217
498
|
},
|
|
499
|
+
]);
|
|
500
|
+
// ============ STEP 2: 모드 선택 및 설정 ============
|
|
501
|
+
console.log(chalk.gray('\n' + '─'.repeat(60)));
|
|
502
|
+
console.log(chalk.gray(' Step 2/3: 변환 설정\n'));
|
|
503
|
+
// 분석 결과 요약 (간략하게)
|
|
504
|
+
console.log(chalk.bold('📊 문서 분석:'));
|
|
505
|
+
const statsLine = [
|
|
506
|
+
`${analysisResult.wordCount.toLocaleString()}단어`,
|
|
507
|
+
`이미지 ${analysisResult.imageCount}개`,
|
|
508
|
+
`표 ${analysisResult.tableCount}개`,
|
|
509
|
+
].join(' | ');
|
|
510
|
+
console.log(chalk.gray(` ${statsLine}`));
|
|
511
|
+
if (analysisResult.issues.length > 0) {
|
|
512
|
+
console.log(chalk.yellow(` ⚠️ ${analysisResult.issues.length}개 이슈 감지 → 자동 최적화 적용됨`));
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
console.log(chalk.green(' ✅ 표준 Markdown - 바로 변환 가능'));
|
|
516
|
+
}
|
|
517
|
+
if (metadata.title) {
|
|
518
|
+
console.log(chalk.gray(` 📖 제목: ${metadata.title}`));
|
|
519
|
+
}
|
|
520
|
+
console.log();
|
|
521
|
+
// 모드 선택
|
|
522
|
+
const modeAnswer = await inquirer.prompt([
|
|
218
523
|
{
|
|
219
524
|
type: 'list',
|
|
220
|
-
name: '
|
|
221
|
-
message: chalk.yellow('
|
|
525
|
+
name: 'mode',
|
|
526
|
+
message: chalk.yellow('🚀 변환 모드 선택:'),
|
|
222
527
|
choices: [
|
|
223
|
-
{
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
528
|
+
{
|
|
529
|
+
name: chalk.green('⚡ 빠른 변환') + chalk.gray(' - 스마트 기본값으로 바로 변환 (권장)'),
|
|
530
|
+
value: 'quick',
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: chalk.blue('⚙️ 상세 설정') + chalk.gray(' - 모든 옵션을 직접 선택'),
|
|
534
|
+
value: 'custom',
|
|
535
|
+
},
|
|
228
536
|
],
|
|
229
|
-
default:
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
type: 'confirm',
|
|
233
|
-
name: 'validateContent',
|
|
234
|
-
message: chalk.yellow('🔍 Enable content validation?'),
|
|
235
|
-
default: true,
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
type: 'confirm',
|
|
239
|
-
name: 'autoFix',
|
|
240
|
-
message: chalk.yellow('🔧 Enable auto-fix for detected issues?'),
|
|
241
|
-
default: true,
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
type: 'input',
|
|
245
|
-
name: 'outputPath',
|
|
246
|
-
message: chalk.yellow('📁 Output directory (leave empty for same as input):'),
|
|
247
|
-
default: '',
|
|
537
|
+
default: 'quick',
|
|
248
538
|
},
|
|
249
539
|
]);
|
|
540
|
+
const mode = modeAnswer.mode;
|
|
541
|
+
// 변환 설정 수집
|
|
542
|
+
let format = 'both';
|
|
543
|
+
let typographyPreset = analysisResult.recommendedPreset;
|
|
544
|
+
let coverTheme = 'apple';
|
|
545
|
+
const inferredTitle = metadata.title || path.basename(resolvedInputPath, '.md');
|
|
546
|
+
const inferredAuthor = metadata.author || '';
|
|
547
|
+
let customTitle = metaAnswers.customTitle.trim() || inferredTitle;
|
|
548
|
+
let customAuthor = metaAnswers.customAuthor.trim() || inferredAuthor;
|
|
549
|
+
let outputPath = '';
|
|
550
|
+
if (mode === 'quick') {
|
|
551
|
+
// 빠른 모드: 출력 형식만 선택
|
|
552
|
+
const quickAnswers = await inquirer.prompt([
|
|
553
|
+
{
|
|
554
|
+
type: 'list',
|
|
555
|
+
name: 'format',
|
|
556
|
+
message: chalk.yellow('📤 출력 형식:'),
|
|
557
|
+
choices: [
|
|
558
|
+
{ name: chalk.magenta('📚 EPUB + PDF'), value: 'both' },
|
|
559
|
+
{ name: chalk.green('📖 EPUB만'), value: 'epub' },
|
|
560
|
+
{ name: chalk.blue('📄 PDF만'), value: 'pdf' },
|
|
561
|
+
],
|
|
562
|
+
default: 'both',
|
|
563
|
+
},
|
|
564
|
+
]);
|
|
565
|
+
format = quickAnswers.format;
|
|
566
|
+
// 스마트 기본값 적용
|
|
567
|
+
console.log(chalk.gray('\n 📋 적용될 설정:'));
|
|
568
|
+
console.log(chalk.gray(` 프리셋: ${TYPOGRAPHY_PRESETS[typographyPreset]?.name || typographyPreset}`));
|
|
569
|
+
console.log(chalk.gray(` 표지: ${COVER_THEMES[coverTheme]?.name || coverTheme}`));
|
|
570
|
+
if (analysisResult.recommendPreprocess) {
|
|
571
|
+
console.log(chalk.gray(' Obsidian 최적화: 자동 적용'));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else if (mode === 'custom') {
|
|
575
|
+
// 상세 모드: 모든 옵션 선택
|
|
576
|
+
const customAnswers = await inquirer.prompt([
|
|
577
|
+
{
|
|
578
|
+
type: 'list',
|
|
579
|
+
name: 'format',
|
|
580
|
+
message: chalk.yellow('📤 출력 형식:'),
|
|
581
|
+
choices: [
|
|
582
|
+
{ name: chalk.magenta('📚 EPUB + PDF'), value: 'both' },
|
|
583
|
+
{ name: chalk.green('📖 EPUB만'), value: 'epub' },
|
|
584
|
+
{ name: chalk.blue('📄 PDF만'), value: 'pdf' },
|
|
585
|
+
],
|
|
586
|
+
default: 'both',
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
type: 'list',
|
|
590
|
+
name: 'typographyPreset',
|
|
591
|
+
message: chalk.yellow('🎨 타이포그래피 프리셋:'),
|
|
592
|
+
choices: [
|
|
593
|
+
...getSimplifiedPresetChoices(analysisResult.recommendedPreset),
|
|
594
|
+
new inquirer.Separator(),
|
|
595
|
+
{ name: chalk.gray('더 많은 프리셋 보기...'), value: '_more' },
|
|
596
|
+
],
|
|
597
|
+
default: analysisResult.recommendedPreset,
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
type: 'list',
|
|
601
|
+
name: 'coverTheme',
|
|
602
|
+
message: chalk.yellow('🖼️ 표지 테마:'),
|
|
603
|
+
choices: [
|
|
604
|
+
...getSimplifiedThemeChoices(),
|
|
605
|
+
new inquirer.Separator(),
|
|
606
|
+
{ name: chalk.gray('더 많은 테마 보기...'), value: '_more' },
|
|
607
|
+
],
|
|
608
|
+
default: 'apple',
|
|
609
|
+
},
|
|
610
|
+
]);
|
|
611
|
+
format = customAnswers.format;
|
|
612
|
+
typographyPreset = customAnswers.typographyPreset;
|
|
613
|
+
coverTheme = customAnswers.coverTheme;
|
|
614
|
+
// "더 보기" 선택 시 전체 목록 표시
|
|
615
|
+
if (typographyPreset === '_more') {
|
|
616
|
+
const morePresetAnswer = await inquirer.prompt([
|
|
617
|
+
{
|
|
618
|
+
type: 'list',
|
|
619
|
+
name: 'typographyPreset',
|
|
620
|
+
message: chalk.yellow('🎨 타이포그래피 프리셋 (전체):'),
|
|
621
|
+
choices: getTypographyPresetChoices(analysisResult),
|
|
622
|
+
default: analysisResult.recommendedPreset,
|
|
623
|
+
},
|
|
624
|
+
]);
|
|
625
|
+
typographyPreset = morePresetAnswer.typographyPreset;
|
|
626
|
+
}
|
|
627
|
+
if (coverTheme === '_more') {
|
|
628
|
+
const moreThemeAnswer = await inquirer.prompt([
|
|
629
|
+
{
|
|
630
|
+
type: 'list',
|
|
631
|
+
name: 'coverTheme',
|
|
632
|
+
message: chalk.yellow('🖼️ 표지 테마 (전체):'),
|
|
633
|
+
choices: getCoverThemeChoices(),
|
|
634
|
+
default: 'apple',
|
|
635
|
+
},
|
|
636
|
+
]);
|
|
637
|
+
coverTheme = moreThemeAnswer.coverTheme;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// ============ STEP 3: 변환 실행 ============
|
|
641
|
+
console.log(chalk.gray('\n' + '─'.repeat(60)));
|
|
642
|
+
console.log(chalk.gray(' Step 3/3: 변환 실행\n'));
|
|
250
643
|
try {
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
644
|
+
// Check dependencies proactively
|
|
645
|
+
const depChecker = new DependencyChecker();
|
|
646
|
+
const isReady = await depChecker.quickCheck(format);
|
|
647
|
+
if (!isReady) {
|
|
648
|
+
await depChecker.displayDependencyReport();
|
|
649
|
+
console.log(chalk.red('\n❌ 필수 의존성을 먼저 설치해 주세요.\n'));
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
const spinner = ora(chalk.cyan('⚙️ 초기화 중...')).start();
|
|
256
653
|
const converter = new MarkdownToDocument();
|
|
257
654
|
const initResult = await converter.initialize();
|
|
258
655
|
if (!initResult.success) {
|
|
259
|
-
spinner.fail(chalk.red('
|
|
260
|
-
console.error(chalk.red(
|
|
656
|
+
spinner.fail(chalk.red('초기화 실패'));
|
|
657
|
+
console.error(chalk.red(`\n❌ ${initResult.error}`));
|
|
261
658
|
console.log(chalk.yellow('\n' + MarkdownToDocument.getInstallInstructions()));
|
|
262
659
|
process.exit(1);
|
|
263
660
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const cleanedInputPath = answers.inputPath.trim().replace(/^['"]|['"]$/g, '');
|
|
661
|
+
// 변환 실행
|
|
662
|
+
spinner.text = chalk.cyan('🔄 문서 변환 중...');
|
|
267
663
|
const conversionOptions = {
|
|
268
|
-
inputPath:
|
|
269
|
-
outputPath:
|
|
270
|
-
format:
|
|
271
|
-
typographyPreset:
|
|
272
|
-
coverTheme:
|
|
273
|
-
validateContent:
|
|
274
|
-
autoFix:
|
|
664
|
+
inputPath: resolvedInputPath,
|
|
665
|
+
outputPath: outputPath ? path.resolve(outputPath) : undefined,
|
|
666
|
+
format: format,
|
|
667
|
+
typographyPreset: typographyPreset,
|
|
668
|
+
coverTheme: coverTheme,
|
|
669
|
+
validateContent: true,
|
|
670
|
+
autoFix: true,
|
|
275
671
|
tocDepth: 2,
|
|
276
672
|
includeToc: true,
|
|
277
|
-
customTitle:
|
|
278
|
-
customAuthor:
|
|
673
|
+
customTitle: customTitle || undefined,
|
|
674
|
+
customAuthor: customAuthor || undefined,
|
|
279
675
|
};
|
|
280
|
-
const convertSpinner = ora({
|
|
281
|
-
text: chalk.cyan('🔄 Converting document...'),
|
|
282
|
-
spinner: 'dots',
|
|
283
|
-
}).start();
|
|
284
676
|
const result = await converter.convert(conversionOptions);
|
|
285
677
|
if (result.success) {
|
|
286
|
-
|
|
287
|
-
console.log(chalk.
|
|
288
|
-
console.log(chalk.green.bold('\n📦 Output Files:\n'));
|
|
678
|
+
spinner.succeed(chalk.green('변환 완료!'));
|
|
679
|
+
console.log(chalk.green.bold('\n📦 생성된 파일:\n'));
|
|
289
680
|
if (result.epubPath) {
|
|
290
|
-
console.log(chalk.green(`
|
|
681
|
+
console.log(chalk.green(` 📖 ${result.epubPath}`));
|
|
291
682
|
}
|
|
292
683
|
if (result.pdfPath) {
|
|
293
|
-
console.log(chalk.blue(`
|
|
684
|
+
console.log(chalk.blue(` 📄 ${result.pdfPath}`));
|
|
294
685
|
}
|
|
295
|
-
console.log(chalk.gray('\n' + '
|
|
296
|
-
console.log(chalk.green.bold('
|
|
686
|
+
console.log(chalk.gray('\n' + '═'.repeat(60)));
|
|
687
|
+
console.log(chalk.green.bold('🎉 변환이 완료되었습니다!\n'));
|
|
297
688
|
}
|
|
298
689
|
else {
|
|
299
|
-
|
|
300
|
-
console.log(chalk.red('\n❌
|
|
690
|
+
spinner.fail(chalk.red('변환 실패'));
|
|
691
|
+
console.log(chalk.red('\n❌ 오류:'));
|
|
301
692
|
result.errors.forEach(error => {
|
|
302
|
-
console.log(chalk.red(`
|
|
693
|
+
console.log(chalk.red(` • ${error}`));
|
|
303
694
|
});
|
|
304
|
-
console.log(chalk.red('\n❌ Conversion failed!\n'));
|
|
305
695
|
process.exit(1);
|
|
306
696
|
}
|
|
307
697
|
}
|
|
@@ -347,16 +737,15 @@ program
|
|
|
347
737
|
.command('check')
|
|
348
738
|
.description('Check if required dependencies are installed')
|
|
349
739
|
.action(async () => {
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
console.log(chalk.
|
|
740
|
+
const depChecker = new DependencyChecker();
|
|
741
|
+
const isReady = await depChecker.displayDependencyReport();
|
|
742
|
+
if (isReady) {
|
|
743
|
+
console.log(chalk.green('🚀 준비 완료! 지금 바로 문서 변환을 시작할 수 있습니다.\n'));
|
|
744
|
+
console.log(chalk.cyan('사용 예시:'));
|
|
745
|
+
console.log(chalk.gray(' m2d document.md'));
|
|
746
|
+
console.log(chalk.gray(' m2d interactive\n'));
|
|
355
747
|
}
|
|
356
748
|
else {
|
|
357
|
-
console.log(chalk.red('❌ Dependency check failed\n'));
|
|
358
|
-
console.log(chalk.yellow(result.error || 'Unknown error'));
|
|
359
|
-
console.log(chalk.yellow('\n' + MarkdownToDocument.getInstallInstructions()));
|
|
360
749
|
process.exit(1);
|
|
361
750
|
}
|
|
362
751
|
});
|