@bytechain.cn/colamd-themes 0.3.0 → 0.3.1

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.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared utility functions for CLI commands.
3
+ * Provides consistent file path resolution and error handling.
4
+ */
5
+ import { existsSync } from "node:fs";
6
+ import { resolve, isAbsolute, basename } from "node:path";
7
+ /**
8
+ * Resolve input file path with validation.
9
+ * Converts relative paths to absolute and checks existence.
10
+ *
11
+ * @param input File path provided by user
12
+ * @returns Resolved absolute path
13
+ * @throws Error if file does not exist
14
+ */
15
+ export function resolveInputPath(input) {
16
+ const path = isAbsolute(input) ? input : resolve(process.cwd(), input);
17
+ if (!existsSync(path)) {
18
+ throw new Error(`File not found: ${path}`);
19
+ }
20
+ return path;
21
+ }
22
+ /**
23
+ * Resolve output file path with sensible defaults.
24
+ * If output path is not specified, derives it from input path by replacing extension.
25
+ *
26
+ * @param inputPath Resolved absolute input path
27
+ * @param output Optional explicit output path
28
+ * @param ext Target extension (default: ".html")
29
+ * @returns Resolved absolute output path
30
+ */
31
+ export function resolveOutputPath(inputPath, output, ext = ".html") {
32
+ if (output) {
33
+ return isAbsolute(output) ? output : resolve(process.cwd(), output);
34
+ }
35
+ return inputPath.replace(/\.md$/i, ext);
36
+ }
37
+ /**
38
+ * Get base filename without extension for use as document title.
39
+ *
40
+ * @param filePath Absolute or relative file path
41
+ * @returns Filename without extension
42
+ */
43
+ export function getDocumentTitle(filePath) {
44
+ return basename(filePath, ".md");
45
+ }
46
+ /**
47
+ * Standard error codes for CLI operations.
48
+ * Enables consistent error handling across all commands.
49
+ */
50
+ export var ErrorCode;
51
+ (function (ErrorCode) {
52
+ /** Input file not found */
53
+ ErrorCode[ErrorCode["FILE_NOT_FOUND"] = 1] = "FILE_NOT_FOUND";
54
+ /** Theme resolution failed */
55
+ ErrorCode[ErrorCode["THEME_ERROR"] = 2] = "THEME_ERROR";
56
+ /** Rendering/export failed */
57
+ ErrorCode[ErrorCode["RENDER_ERROR"] = 3] = "RENDER_ERROR";
58
+ /** Invalid arguments or configuration */
59
+ ErrorCode[ErrorCode["INVALID_INPUT"] = 4] = "INVALID_INPUT";
60
+ /** Unknown/unexpected error */
61
+ ErrorCode[ErrorCode["UNKNOWN"] = 99] = "UNKNOWN";
62
+ })(ErrorCode || (ErrorCode = {}));
63
+ /**
64
+ * Custom error class for CLI operations with structured error codes.
65
+ * Replaces direct process.exit() calls for better error propagation.
66
+ */
67
+ export class CLIError extends Error {
68
+ code;
69
+ cause;
70
+ constructor(message, code = ErrorCode.UNKNOWN, cause) {
71
+ super(message);
72
+ this.code = code;
73
+ this.cause = cause;
74
+ this.name = "CLIError";
75
+ if (cause) {
76
+ this.stack = `${this.message}\nCaused by: ${cause.stack}`;
77
+ }
78
+ }
79
+ }
80
+ //# sourceMappingURL=cli-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-utils.js","sourceRoot":"","sources":["../../src/utils/cli-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1D;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,KAAK,CAAC,CAAC;IACvE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,MAAe,EAAE,GAAG,GAAG,OAAO;IACjF,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,OAAO,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAN,IAAY,SAWX;AAXD,WAAY,SAAS;IACnB,2BAA2B;IAC3B,6DAAkB,CAAA;IAClB,8BAA8B;IAC9B,uDAAe,CAAA;IACf,8BAA8B;IAC9B,yDAAgB,CAAA;IAChB,yCAAyC;IACzC,2DAAiB,CAAA;IACjB,+BAA+B;IAC/B,gDAAY,CAAA;AACd,CAAC,EAXW,SAAS,KAAT,SAAS,QAWpB;AAED;;;GAGG;AACH,MAAM,OAAO,QAAS,SAAQ,KAAK;IAGxB;IACA;IAHT,YACE,OAAe,EACR,OAAkB,SAAS,CAAC,OAAO,EACnC,KAAa;QAEpB,KAAK,CAAC,OAAO,CAAC,CAAC;QAHR,SAAI,GAAJ,IAAI,CAA+B;QACnC,UAAK,GAAL,KAAK,CAAQ;QAGpB,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,KAAK,GAAG,GAAG,IAAI,CAAC,OAAO,gBAAgB,KAAK,CAAC,KAAK,EAAE,CAAC;QAC5D,CAAC;IACH,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytechain.cn/colamd-themes",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Theme development & export tool for ColaMD — extract formatting, generate themes, and export Markdown to HTML/PDF",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/cli.ts CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { Command } from "commander";
4
4
  import { existsSync, readdirSync, readFileSync } from "node:fs";
5
- import { join, isAbsolute } from "node:path";
5
+ import { join, isAbsolute, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
6
7
  import chalk from "chalk";
7
8
  import ora from "ora";
8
9
  import { UrlExtractor } from "./extractors/url-extractor.js";
@@ -16,12 +17,28 @@ import { validateSeedPalette, summarizeIssues } from "./validators.js";
16
17
  import type { ThemeStyle } from "./models.js";
17
18
  import { registerExportCommands } from "./export-cli.js";
18
19
 
20
+ /**
21
+ * Dynamically read version from package.json to avoid hardcoding.
22
+ * This ensures CLI --version always matches the published package version.
23
+ */
24
+ function getPackageVersion(): string {
25
+ try {
26
+ const __filename = fileURLToPath(import.meta.url);
27
+ const __dirname = dirname(__filename);
28
+ const pkgPath = join(__dirname, "../package.json");
29
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
30
+ return pkg.version ?? "0.0.0";
31
+ } catch {
32
+ return "0.0.0";
33
+ }
34
+ }
35
+
19
36
  const program = new Command();
20
37
 
21
38
  program
22
39
  .name("colamd-themes")
23
40
  .description("ColaMD Theme Development Tool — extract formatting and generate v3.0 paradigm CSS themes")
24
- .version("0.2.0");
41
+ .version(getPackageVersion());
25
42
 
26
43
  // ── Shared extract options builder ──
27
44
 
package/src/export-cli.ts CHANGED
@@ -2,11 +2,12 @@
2
2
  * Export CLI commands: set-theme, export-html, export-pdf, export (batch).
3
3
  *
4
4
  * These are registered onto the main Commander program in cli.ts.
5
+ * Uses unified error handling via CLIError and shared utility functions.
5
6
  */
6
7
 
7
8
  import { Command } from "commander";
8
- import { existsSync, readFileSync, readdirSync, statSync, mkdirSync } from "node:fs";
9
- import { join, isAbsolute, resolve, basename } from "node:path";
9
+ import { readFileSync, readdirSync, statSync, mkdirSync } from "node:fs";
10
+ import { join, resolve } from "node:path";
10
11
  import chalk from "chalk";
11
12
  import ora from "ora";
12
13
  import {
@@ -18,6 +19,13 @@ import {
18
19
  resolveTheme,
19
20
  } from "./theme-store.js";
20
21
  import { ColamdRenderer, type RenderInput } from "./renderer.js";
22
+ import {
23
+ resolveInputPath,
24
+ resolveOutputPath,
25
+ getDocumentTitle,
26
+ CLIError,
27
+ ErrorCode,
28
+ } from "./utils/cli-utils.js";
21
29
 
22
30
  // ── set-theme ──
23
31
 
@@ -29,54 +37,62 @@ function registerSetTheme(program: Command): void {
29
37
  .option("--css <file>", "Register as custom theme using this CSS file")
30
38
  .option("--default", "Set as the default export theme")
31
39
  .option("--remove", "Unregister a custom theme")
32
- .action((name: string, options: { css?: string; default?: boolean; remove?: boolean }) => {
33
- // Remove
34
- if (options.remove) {
35
- if (storeUnregister(name)) {
36
- console.log(chalk.green(`✔ Unregistered custom theme "${name}"`));
37
- } else {
38
- console.log(chalk.yellow(`Theme "${name}" is not registered.`));
40
+ .action(async (name: string, options: { css?: string; default?: boolean; remove?: boolean }) => {
41
+ try {
42
+ // Remove
43
+ if (options.remove) {
44
+ const removed = await storeUnregister(name);
45
+ if (removed) {
46
+ console.log(chalk.green(`✔ Unregistered custom theme "${name}"`));
47
+ } else {
48
+ console.log(chalk.yellow(`Theme "${name}" is not registered.`));
49
+ }
50
+ return;
39
51
  }
40
- return;
41
- }
42
52
 
43
- // Register custom theme
44
- if (options.css) {
45
- const cssPath = isAbsolute(options.css) ? options.css : resolve(process.cwd(), options.css);
46
- if (!existsSync(cssPath)) {
47
- console.error(chalk.red(`CSS file not found: ${cssPath}`));
48
- process.exit(1);
53
+ // Register custom theme
54
+ if (options.css) {
55
+ const cssPath = resolveInputPath(options.css);
56
+ await storeRegister(name, cssPath);
57
+ console.log(chalk.green(`✔ Registered custom theme "${name}" → ${cssPath}`));
58
+ if (options.default) {
59
+ await storeSetDefault(name);
60
+ console.log(chalk.green(`✔ Set "${name}" as default theme`));
61
+ }
62
+ return;
49
63
  }
50
- storeRegister(name, cssPath);
51
- console.log(chalk.green(`✔ Registered custom theme "${name}" → ${cssPath}`));
52
- if (options.default) {
53
- storeSetDefault(name);
54
- console.log(chalk.green(`✔ Set "${name}" as default theme`));
64
+
65
+ // Set as default (built-in or already registered)
66
+ if (!BUILTIN_THEMES.includes(name as any)) {
67
+ const config = await getConfig();
68
+ if (!(name in config.themes)) {
69
+ throw new CLIError(
70
+ `Unknown theme "${name}". ` +
71
+ chalk.dim(`Built-in: ${BUILTIN_THEMES.join(", ")}. `) +
72
+ chalk.dim("Or register a custom theme with --css <file>."),
73
+ ErrorCode.THEME_ERROR
74
+ );
75
+ }
55
76
  }
56
- return;
57
- }
58
77
 
59
- // Set as default (built-in or already registered)
60
- if (!BUILTIN_THEMES.includes(name as any)) {
61
- const config = getConfig();
62
- if (!(name in config.themes)) {
63
- console.error(
64
- chalk.red(`Unknown theme "${name}". `) +
65
- chalk.dim(`Built-in: ${BUILTIN_THEMES.join(", ")}. `) +
66
- chalk.dim("Or register a custom theme with --css <file>.")
67
- );
68
- process.exit(1);
78
+ await storeSetDefault(name);
79
+ console.log(chalk.green(`✔ Default theme set to "${name}"`));
80
+
81
+ // Show current config
82
+ const config = await getConfig();
83
+ console.log(chalk.dim(`\n Default: ${config.defaultTheme}`));
84
+ const customs = Object.keys(config.themes);
85
+ if (customs.length) {
86
+ console.log(chalk.dim(` Custom themes: ${customs.join(", ")}`));
87
+ }
88
+ } catch (err) {
89
+ if (err instanceof CLIError) {
90
+ console.error(chalk.red(err.message));
91
+ process.exit(err.code);
92
+ } else {
93
+ console.error(chalk.red(`Unexpected error: ${(err as Error).message}`));
94
+ process.exit(ErrorCode.UNKNOWN);
69
95
  }
70
- }
71
- storeSetDefault(name);
72
- console.log(chalk.green(`✔ Default theme set to "${name}"`));
73
-
74
- // Show current config
75
- const config = getConfig();
76
- console.log(chalk.dim(`\n Default: ${config.defaultTheme}`));
77
- const customs = Object.keys(config.themes);
78
- if (customs.length) {
79
- console.log(chalk.dim(` Custom themes: ${customs.join(", ")}`));
80
96
  }
81
97
  });
82
98
  }
@@ -91,42 +107,47 @@ function registerExportHTML(program: Command): void {
91
107
  .option("-t, --theme <name>", "Theme name (built-in, custom, or CSS file path)")
92
108
  .option("-o, --output <path>", "Output HTML file path")
93
109
  .action(async (input: string, options: { theme?: string; output?: string }) => {
94
- const inputPath = isAbsolute(input) ? input : resolve(process.cwd(), input);
95
- if (!existsSync(inputPath)) {
96
- console.error(chalk.red(`File not found: ${inputPath}`));
97
- process.exit(1);
98
- }
99
-
100
- const md = readFileSync(inputPath, "utf-8");
101
- const title = basename(inputPath, ".md");
102
- const outputPath = options.output
103
- ? (isAbsolute(options.output) ? options.output : resolve(process.cwd(), options.output))
104
- : inputPath.replace(/\.md$/i, ".html");
105
-
106
- let theme;
107
- try {
108
- theme = resolveTheme(options.theme);
109
- } catch (err) {
110
- console.error(chalk.red((err as Error).message));
111
- process.exit(1);
112
- }
113
-
114
- const spinner = ora(`Rendering ${basename(inputPath)} → HTML (${theme.themeName})...`).start();
115
110
  try {
116
- const { renderMarkdown } = await import("./renderer.js");
117
- const result = await renderMarkdown({
118
- markdown: md,
119
- themeName: theme.themeName,
120
- customCSS: theme.customCSS,
121
- title,
122
- });
123
-
124
- const { writeFileSync } = await import("node:fs");
125
- writeFileSync(outputPath, result.html, "utf-8");
126
- spinner.succeed(chalk.green(`Exported: ${outputPath}`));
111
+ // Resolve paths using shared utilities
112
+ const inputPath = resolveInputPath(input);
113
+ const title = getDocumentTitle(inputPath);
114
+ const outputPath = resolveOutputPath(inputPath, options.output, ".html");
115
+
116
+ // Read markdown content
117
+ const md = readFileSync(inputPath, "utf-8");
118
+
119
+ // Resolve theme with async API
120
+ const theme = await resolveTheme(options.theme);
121
+
122
+ // Render with progress indicator
123
+ const spinner = ora(`Rendering ${title} → HTML (${theme.themeName})...`).start();
124
+ try {
125
+ const { renderMarkdown } = await import("./renderer.js");
126
+ const result = await renderMarkdown({
127
+ markdown: md,
128
+ themeName: theme.themeName,
129
+ customCSS: theme.customCSS,
130
+ title,
131
+ });
132
+
133
+ const { writeFileSync } = await import("node:fs");
134
+ writeFileSync(outputPath, result.html, "utf-8");
135
+ spinner.succeed(chalk.green(`Exported: ${outputPath}`));
136
+ } catch (renderErr) {
137
+ spinner.fail(chalk.red(`Export failed: ${(renderErr as Error).message}`));
138
+ throw new CLIError(
139
+ `HTML rendering failed: ${(renderErr as Error).message}`,
140
+ ErrorCode.RENDER_ERROR,
141
+ renderErr as Error
142
+ );
143
+ }
127
144
  } catch (err) {
128
- spinner.fail(chalk.red(`Export failed: ${(err as Error).message}`));
129
- process.exit(1);
145
+ if (err instanceof CLIError) {
146
+ process.exit(err.code);
147
+ } else {
148
+ console.error(chalk.red((err as Error).message));
149
+ process.exit(ErrorCode.FILE_NOT_FOUND);
150
+ }
130
151
  }
131
152
  });
132
153
  }
@@ -142,45 +163,59 @@ function registerExportPDF(program: Command): void {
142
163
  .option("-o, --output <path>", "Output PDF file path")
143
164
  .option("--format <size>", "Page format: A4, Letter, A3", "A4")
144
165
  .action(async (input: string, options: { theme?: string; output?: string; format?: string }) => {
145
- const inputPath = isAbsolute(input) ? input : resolve(process.cwd(), input);
146
- if (!existsSync(inputPath)) {
147
- console.error(chalk.red(`File not found: ${inputPath}`));
148
- process.exit(1);
149
- }
150
-
151
- const md = readFileSync(inputPath, "utf-8");
152
- const title = basename(inputPath, ".md");
153
- const outputPath = options.output
154
- ? (isAbsolute(options.output) ? options.output : resolve(process.cwd(), options.output))
155
- : inputPath.replace(/\.md$/i, ".pdf");
156
-
157
- let theme;
158
166
  try {
159
- theme = resolveTheme(options.theme);
160
- } catch (err) {
161
- console.error(chalk.red((err as Error).message));
162
- process.exit(1);
163
- }
167
+ // Resolve paths using shared utilities
168
+ const inputPath = resolveInputPath(input);
169
+ const title = getDocumentTitle(inputPath);
170
+ const outputPath = resolveOutputPath(inputPath, options.output, ".pdf");
171
+
172
+ // Read markdown content
173
+ const md = readFileSync(inputPath, "utf-8");
174
+
175
+ // Validate format option
176
+ const validFormats = ["A4", "Letter", "A3"];
177
+ if (!validFormats.includes(options.format ?? "")) {
178
+ throw new CLIError(
179
+ `Invalid page format: "${options.format}". Valid formats: ${validFormats.join(", ")}`,
180
+ ErrorCode.INVALID_INPUT
181
+ );
182
+ }
164
183
 
165
- const spinner = ora(`Rendering ${basename(inputPath)} → PDF (${theme.themeName})...`).start();
166
- try {
167
- const { renderPDF } = await import("./renderer.js");
168
- const pdfBuffer = await renderPDF(
169
- {
170
- markdown: md,
171
- themeName: theme.themeName,
172
- customCSS: theme.customCSS,
173
- title,
174
- },
175
- { format: options.format as any }
176
- );
184
+ // Resolve theme with async API
185
+ const theme = await resolveTheme(options.theme);
177
186
 
178
- const { writeFileSync } = await import("node:fs");
179
- writeFileSync(outputPath, pdfBuffer);
180
- spinner.succeed(chalk.green(`Exported: ${outputPath}`));
187
+ // Render PDF with progress indicator
188
+ const spinner = ora(`Rendering ${title} → PDF (${theme.themeName})...`).start();
189
+ try {
190
+ const { renderPDF } = await import("./renderer.js");
191
+ const pdfBuffer = await renderPDF(
192
+ {
193
+ markdown: md,
194
+ themeName: theme.themeName,
195
+ customCSS: theme.customCSS,
196
+ title,
197
+ },
198
+ { format: options.format as any }
199
+ );
200
+
201
+ const { writeFileSync } = await import("node:fs");
202
+ writeFileSync(outputPath, pdfBuffer);
203
+ spinner.succeed(chalk.green(`Exported: ${outputPath}`));
204
+ } catch (renderErr) {
205
+ spinner.fail(chalk.red(`Export failed: ${(renderErr as Error).message}`));
206
+ throw new CLIError(
207
+ `PDF rendering failed: ${(renderErr as Error).message}`,
208
+ ErrorCode.RENDER_ERROR,
209
+ renderErr as Error
210
+ );
211
+ }
181
212
  } catch (err) {
182
- spinner.fail(chalk.red(`Export failed: ${(err as Error).message}`));
183
- process.exit(1);
213
+ if (err instanceof CLIError) {
214
+ process.exit(err.code);
215
+ } else {
216
+ console.error(chalk.red((err as Error).message));
217
+ process.exit(ErrorCode.FILE_NOT_FOUND);
218
+ }
184
219
  }
185
220
  });
186
221
  }
@@ -196,97 +231,117 @@ function registerExport(program: Command): void {
196
231
  .option("-t, --theme <name>", "Theme name")
197
232
  .option("-d, --output-dir <dir>", "Output directory", ".")
198
233
  .action(async (inputs: string[], options: { format?: string; theme?: string; outputDir?: string }) => {
199
- // Resolve glob inputs (shell usually expands, but handle edge cases)
200
- const files: string[] = [];
201
- for (const input of inputs) {
202
- const p = isAbsolute(input) ? input : resolve(process.cwd(), input);
203
- if (existsSync(p)) {
204
- if (statSync(p).isDirectory()) {
205
- // Directory: include all .md files
206
- const dirFiles = readdirSync(p)
207
- .filter((f) => f.endsWith(".md"))
208
- .map((f) => join(p, f));
209
- files.push(...dirFiles);
210
- } else {
211
- files.push(p);
234
+ try {
235
+ // Resolve glob inputs (shell usually expands, but handle edge cases)
236
+ const files: string[] = [];
237
+ for (const input of inputs) {
238
+ try {
239
+ const p = resolveInputPath(input);
240
+ if (statSync(p).isDirectory()) {
241
+ // Directory: include all .md files
242
+ const dirFiles = readdirSync(p)
243
+ .filter((f) => f.endsWith(".md"))
244
+ .map((f) => join(p, f));
245
+ files.push(...dirFiles);
246
+ } else {
247
+ files.push(p);
248
+ }
249
+ } catch {
250
+ console.log(chalk.yellow(`Skipping (not found): ${input}`));
212
251
  }
213
- } else {
214
- console.log(chalk.yellow(`Skipping (not found): ${input}`));
215
252
  }
216
- }
217
-
218
- if (files.length === 0) {
219
- console.error(chalk.red("No valid input files found."));
220
- process.exit(1);
221
- }
222
-
223
- let theme;
224
- try {
225
- theme = resolveTheme(options.theme);
226
- } catch (err) {
227
- console.error(chalk.red((err as Error).message));
228
- process.exit(1);
229
- }
230
253
 
231
- const outDir = isAbsolute(options.outputDir ?? ".")
232
- ? (options.outputDir ?? ".")
233
- : resolve(process.cwd(), options.outputDir ?? ".");
234
- const fmt = options.format ?? "html";
235
-
236
- // Ensure output directory exists
237
- if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
238
-
239
- console.log(chalk.bold(`\nBatch export: ${files.length} file(s) → ${fmt.toUpperCase()}`));
240
- console.log(chalk.dim(` Theme: ${theme.themeName}`));
241
- console.log(chalk.dim(` Output: ${outDir}/\n`));
242
-
243
- const renderer = new ColamdRenderer();
244
- let success = 0;
245
- let failed = 0;
254
+ if (files.length === 0) {
255
+ throw new CLIError("No valid input files found.", ErrorCode.FILE_NOT_FOUND);
256
+ }
246
257
 
247
- try {
248
- await renderer.init();
258
+ // Resolve theme with async API
259
+ const theme = await resolveTheme(options.theme);
249
260
 
250
- for (const file of files) {
251
- const name = basename(file, ".md");
252
- const md = readFileSync(file, "utf-8");
253
- const outPath = join(outDir, `${name}.${fmt === "pdf" ? "pdf" : "html"}`);
254
- const spinner = ora(` ${name}.md`).start();
261
+ // Setup output directory
262
+ const outDir = resolve(process.cwd(), options.outputDir ?? ".");
263
+ const fmt = options.format ?? "html";
255
264
 
256
- try {
257
- const input: RenderInput = {
258
- markdown: md,
259
- themeName: theme.themeName,
260
- customCSS: theme.customCSS,
261
- title: name,
262
- };
265
+ if (!["html", "pdf"].includes(fmt)) {
266
+ throw new CLIError(
267
+ `Invalid format: "${fmt}". Supported formats: html, pdf`,
268
+ ErrorCode.INVALID_INPUT
269
+ );
270
+ }
263
271
 
264
- if (fmt === "pdf") {
265
- const pdfBuffer = await renderer.exportPDF(input);
266
- const { writeFileSync } = await import("node:fs");
267
- writeFileSync(outPath, pdfBuffer);
268
- } else {
269
- const result = await renderer.render(input);
270
- const { writeFileSync } = await import("node:fs");
271
- writeFileSync(outPath, result.html, "utf-8");
272
+ // Ensure output directory exists
273
+ const { existsSync: dirExists } = await import("node:fs");
274
+ if (!dirExists(outDir)) mkdirSync(outDir, { recursive: true });
275
+
276
+ console.log(chalk.bold(`\nBatch export: ${files.length} file(s) → ${fmt.toUpperCase()}`));
277
+ console.log(chalk.dim(` Theme: ${theme.themeName}`));
278
+ console.log(chalk.dim(` Output: ${outDir}/\n`));
279
+
280
+ // Initialize renderer for batch processing
281
+ const renderer = new ColamdRenderer();
282
+ let success = 0;
283
+ let failed = 0;
284
+
285
+ try {
286
+ await renderer.init();
287
+
288
+ for (const file of files) {
289
+ const name = getDocumentTitle(file);
290
+ const md = readFileSync(file, "utf-8");
291
+ const outPath = join(outDir, `${name}.${fmt === "pdf" ? "pdf" : "html"}`);
292
+ const spinner = ora(` ${name}.md`).start();
293
+
294
+ try {
295
+ const input: RenderInput = {
296
+ markdown: md,
297
+ themeName: theme.themeName,
298
+ customCSS: theme.customCSS,
299
+ title: name,
300
+ };
301
+
302
+ if (fmt === "pdf") {
303
+ const pdfBuffer = await renderer.exportPDF(input);
304
+ const { writeFileSync } = await import("node:fs");
305
+ writeFileSync(outPath, pdfBuffer);
306
+ } else {
307
+ const result = await renderer.render(input);
308
+ const { writeFileSync } = await import("node:fs");
309
+ writeFileSync(outPath, result.html, "utf-8");
310
+ }
311
+
312
+ spinner.succeed(`${name}.${fmt === "pdf" ? "pdf" : "html"}`);
313
+ success++;
314
+ } catch (fileErr) {
315
+ spinner.fail(`${name}.md — ${(fileErr as Error).message}`);
316
+ failed++;
272
317
  }
273
-
274
- spinner.succeed(`${name}.${fmt === "pdf" ? "pdf" : "html"}`);
275
- success++;
276
- } catch (err) {
277
- spinner.fail(`${name}.md — ${(err as Error).message}`);
278
- failed++;
279
318
  }
319
+ } finally {
320
+ await renderer.close();
280
321
  }
281
- } finally {
282
- await renderer.close();
283
- }
284
322
 
285
- console.log(
286
- chalk.bold(`\nDone: ${chalk.green(`${success} exported`)}`) +
287
- (failed ? chalk.red(`, ${failed} failed`) : "") +
288
- "\n"
289
- );
323
+ // Summary
324
+ console.log(
325
+ chalk.bold(`\nDone: ${chalk.green(`${success} exported`)}`) +
326
+ (failed ? chalk.red(`, ${failed} failed`) : "") +
327
+ "\n"
328
+ );
329
+
330
+ if (failed > 0) {
331
+ throw new CLIError(
332
+ `${failed} file(s) failed to export`,
333
+ ErrorCode.RENDER_ERROR
334
+ );
335
+ }
336
+ } catch (err) {
337
+ if (err instanceof CLIError) {
338
+ console.error(chalk.red(err.message));
339
+ process.exit(err.code);
340
+ } else {
341
+ console.error(chalk.red((err as Error).message));
342
+ process.exit(ErrorCode.UNKNOWN);
343
+ }
344
+ }
290
345
  });
291
346
  }
292
347