@bytechain.cn/colamd-themes 0.3.2 → 0.5.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.
Files changed (99) hide show
  1. package/.claude/skills/colamd-themes-preview.skill.md +333 -0
  2. package/.claude/skills/colamd-themes.skill.md +10 -0
  3. package/.dsh/skills/colamd-themes/SKILL.md +14 -0
  4. package/.github/supported-topics.yml +17 -0
  5. package/.hermes/skills/tools/colamd-themes/SKILL.md +14 -0
  6. package/.openclaw/skills/colamd-themes/SKILL.md +1 -37
  7. package/.opencode/skills/colamd-themes/SKILL.md +17 -0
  8. package/.trae/rules/colamd-themes.md +0 -36
  9. package/CHANGELOG.md +331 -241
  10. package/DSH_SUPPORT_SUMMARY.md +87 -0
  11. package/README.md +414 -384
  12. package/README_CN.md +412 -384
  13. package/RELEASENOTE.md +541 -389
  14. package/dist/cli.js +36 -55
  15. package/dist/cli.js.map +1 -1
  16. package/dist/color-utils.d.ts +1 -0
  17. package/dist/color-utils.js +25 -6
  18. package/dist/color-utils.js.map +1 -1
  19. package/dist/extractors/pdf-extractor.js +6 -1
  20. package/dist/extractors/pdf-extractor.js.map +1 -1
  21. package/dist/extractors/url-extractor.d.ts +0 -4
  22. package/dist/extractors/url-extractor.js +13 -126
  23. package/dist/extractors/url-extractor.js.map +1 -1
  24. package/dist/generator.d.ts +1 -1
  25. package/dist/generator.js +32 -11
  26. package/dist/generator.js.map +1 -1
  27. package/dist/models.d.ts +5 -4
  28. package/dist/models.js +2 -3
  29. package/dist/models.js.map +1 -1
  30. package/dist/renderer.d.ts +6 -3
  31. package/dist/renderer.js +54 -93
  32. package/dist/renderer.js.map +1 -1
  33. package/dist/templates/template.css +220 -498
  34. package/dist/templates/theme-paradigm.md +530 -658
  35. package/dist/templates/theme-template.hbs +256 -370
  36. package/dist/theme-store.js +19 -3
  37. package/dist/theme-store.js.map +1 -1
  38. package/dist/utils/document-paradigm.d.ts +12 -0
  39. package/dist/utils/document-paradigm.js +109 -0
  40. package/dist/utils/document-paradigm.js.map +1 -0
  41. package/dist/validators.d.ts +4 -1
  42. package/dist/validators.js +115 -1
  43. package/dist/validators.js.map +1 -1
  44. package/mcp-server/dist/index.d.ts +13 -0
  45. package/mcp-server/dist/index.d.ts.map +1 -0
  46. package/mcp-server/dist/index.js +0 -0
  47. package/mcp-server/dist/index.js.map +1 -0
  48. package/mcp-server/package-lock.json +1759 -0
  49. package/mcp-server/package.json +24 -0
  50. package/mcp-server/src/index.ts +0 -0
  51. package/mcp-server/tsconfig.json +19 -0
  52. package/package.json +54 -36
  53. package/skills/build-skills.sh +30 -5
  54. package/skills/colamd-themes/SKILL.md +180 -178
  55. package/src/cli.ts +474 -487
  56. package/src/color-systems.ts +289 -289
  57. package/src/color-utils.ts +137 -120
  58. package/src/contrast.ts +66 -66
  59. package/src/docx-style-paradigm.ts +811 -811
  60. package/src/export-cli.ts +356 -356
  61. package/src/extractors/base.ts +8 -8
  62. package/src/extractors/docx-extractor.ts +417 -417
  63. package/src/extractors/pdf-extractor.ts +533 -527
  64. package/src/extractors/url-extractor.ts +846 -981
  65. package/src/generator.ts +197 -169
  66. package/src/index.ts +29 -29
  67. package/src/models.ts +212 -212
  68. package/src/renderer.ts +310 -359
  69. package/src/templates/template.css +220 -498
  70. package/src/templates/theme-paradigm.md +530 -658
  71. package/src/templates/theme-template.hbs +256 -370
  72. package/src/theme-store.ts +185 -170
  73. package/src/types/pdf-parse.d.ts +12 -12
  74. package/src/utils/cli-utils.ts +84 -84
  75. package/src/utils/document-paradigm.ts +118 -0
  76. package/src/validators.ts +298 -176
  77. package/themes/academic-paper-pt.css +353 -1805
  78. package/themes/academic-paper.css +350 -1099
  79. package/themes/academic-style.css +342 -2430
  80. package/themes/colly.css +256 -370
  81. package/themes/elegant.css +288 -671
  82. package/themes/forest-ink.css +240 -664
  83. package/themes/guizang.css +250 -732
  84. package/themes/hermes-security.css +279 -0
  85. package/themes/meeting-minutes.css +256 -370
  86. package/themes/pixso-design.css +278 -1261
  87. package/themes/salvator-blog.css +256 -370
  88. package/themes/shimo-theme.css +256 -0
  89. package/themes/swiss-design.css +285 -746
  90. package/themes/template.css +220 -498
  91. package/themes/trae-blue.css +280 -737
  92. package/themes/word-contract.css +249 -0
  93. package/themes-preview/demo-content.md +187 -187
  94. package/tsconfig.json +18 -18
  95. package/src/mermaid-presets.ts +0 -142
  96. package/themes/swiss-design copy.css +0 -746
  97. package/themes-preview/colly-preview.html +0 -623
  98. package/themes-preview/meeting-minutes-preview.html +0 -625
  99. package/themes-preview/salvator-blog-preview.html +0 -623
@@ -1,170 +1,185 @@
1
- /**
2
- * Theme configuration persistence.
3
- *
4
- * Stores default theme and registered custom themes in ~/.colamd-themes/config.json
5
- * Uses async file I/O to avoid blocking the event loop.
6
- */
7
-
8
- import { mkdir, readFile, writeFile, access } from "node:fs/promises";
9
- import { statSync } from "node:fs";
10
- import { join, basename } from "node:path";
11
- import { homedir } from "node:os";
12
-
13
- const CONFIG_DIR = join(homedir(), ".colamd-themes");
14
- const CONFIG_FILE = join(CONFIG_DIR, "config.json");
15
-
16
- /** Maximum allowed CSS file size (1MB) to prevent OOM attacks */
17
- const MAX_CSS_SIZE = 1024 * 1024;
18
-
19
- /** Built-in themes provided by @bytechain.cn/colamd */
20
- export const BUILTIN_THEMES = ["light", "dark", "elegant", "newsprint"] as const;
21
- export type BuiltinTheme = (typeof BUILTIN_THEMES)[number];
22
-
23
- export interface ThemeConfig {
24
- /** Default theme name (built-in or custom) */
25
- defaultTheme: string;
26
- /** Registered custom themes: name → CSS file path */
27
- themes: Record<string, string>;
28
- }
29
-
30
- async function readConfig(): Promise<ThemeConfig> {
31
- try {
32
- await access(CONFIG_FILE);
33
- const data = await readFile(CONFIG_FILE, "utf-8");
34
- return JSON.parse(data) as ThemeConfig;
35
- } catch {
36
- return { defaultTheme: "elegant", themes: {} };
37
- }
38
- }
39
-
40
- async function writeConfig(config: ThemeConfig): Promise<void> {
41
- try {
42
- await access(CONFIG_DIR);
43
- } catch {
44
- await mkdir(CONFIG_DIR, { recursive: true });
45
- }
46
- await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
47
- }
48
-
49
- /** Get the full config. */
50
- export async function getConfig(): Promise<ThemeConfig> {
51
- return readConfig();
52
- }
53
-
54
- /** Set the default theme (built-in name or registered custom name). */
55
- export async function setDefaultTheme(name: string): Promise<void> {
56
- const config = await readConfig();
57
- config.defaultTheme = name;
58
- await writeConfig(config);
59
- }
60
-
61
- /**
62
- * Register a custom theme.
63
- * @param name Unique name (will be prefixed with "custom:" when applied)
64
- * @param cssPath Absolute path to the CSS file
65
- */
66
- export async function registerTheme(name: string, cssPath: string): Promise<void> {
67
- const config = await readConfig();
68
- config.themes[name] = cssPath;
69
- await writeConfig(config);
70
- }
71
-
72
- /** Remove a registered custom theme. */
73
- export async function unregisterTheme(name: string): Promise<boolean> {
74
- const config = await readConfig();
75
- if (!(name in config.themes)) return false;
76
- delete config.themes[name];
77
- await writeConfig(config);
78
- return true;
79
- }
80
-
81
- /**
82
- * Sanitize theme name from file path.
83
- * Removes special characters and limits length to prevent injection.
84
- */
85
- function sanitizeName(path: string): string {
86
- return basename(path, ".css")
87
- .replace(/[^a-zA-Z0-9_-]/g, "-")
88
- .replace(/-+/g, "-")
89
- .slice(0, 50) || "custom";
90
- }
91
-
92
- /**
93
- * Validate CSS file before reading.
94
- * Checks extension, size, and basic content validity.
95
- */
96
- async function validateCssFile(filePath: string): Promise<void> {
97
- // Check file extension
98
- if (!filePath.toLowerCase().endsWith(".css")) {
99
- throw new Error(`Theme file must have .css extension: ${filePath}`);
100
- }
101
-
102
- // Check file size (synchronous stat is acceptable for metadata)
103
- let stats;
104
- try {
105
- stats = statSync(filePath);
106
- } catch {
107
- throw new Error(`Cannot access theme file: ${filePath}`);
108
- }
109
-
110
- if (stats.size > MAX_CSS_SIZE) {
111
- throw new Error(
112
- `Theme file too large (${(stats.size / 1024).toFixed(1)}KB). ` +
113
- `Maximum size: ${(MAX_CSS_SIZE / 1024).toFixed(0)}MB`
114
- );
115
- }
116
-
117
- // Read and validate content
118
- const content = await readFile(filePath, "utf-8");
119
- if (content.length < 10) {
120
- throw new Error(`Theme file appears to be empty or invalid: ${filePath}`);
121
- }
122
- }
123
-
124
- /**
125
- * Resolve a theme name to the arguments needed by the ColaMD renderer.
126
- *
127
- * @param name Theme name. If omitted, uses the configured default.
128
- * @returns { themeName, customCSS? } — themeName is either a built-in name
129
- * or "custom:<name>"; customCSS is the CSS content for custom themes.
130
- */
131
- export async function resolveTheme(name?: string): Promise<{
132
- themeName: string;
133
- customCSS?: string;
134
- }> {
135
- const config = await readConfig();
136
- const resolved = name ?? config.defaultTheme;
137
-
138
- // Check if it's a registered custom theme
139
- if (config.themes[resolved]) {
140
- const cssPath = config.themes[resolved];
141
- await validateCssFile(cssPath);
142
- const customCSS = await readFile(cssPath, "utf-8");
143
- return { themeName: `custom:${resolved}`, customCSS };
144
- }
145
-
146
- // Check if it's a built-in theme
147
- if (BUILTIN_THEMES.includes(resolved as BuiltinTheme)) {
148
- return { themeName: resolved };
149
- }
150
-
151
- // Treat as a direct CSS file path
152
- try {
153
- await access(resolved);
154
- } catch {
155
- throw new Error(
156
- `Unknown theme: "${resolved}". Use a built-in theme (${BUILTIN_THEMES.join(", ")}), ` +
157
- `a registered custom theme, or a path to a .css file.`
158
- );
159
- }
160
-
161
- // Validate and read direct CSS file
162
- await validateCssFile(resolved);
163
- const nameFromPath = sanitizeName(resolved);
164
- const customCSS = await readFile(resolved, "utf-8");
165
-
166
- return {
167
- themeName: `custom:${nameFromPath}`,
168
- customCSS,
169
- };
170
- }
1
+ /**
2
+ * Theme configuration persistence.
3
+ *
4
+ * Stores default theme and registered custom themes in ~/.colamd-themes/config.json
5
+ * Uses async file I/O to avoid blocking the event loop.
6
+ */
7
+
8
+ import { mkdir, readFile, writeFile, access, rename } from "node:fs/promises";
9
+ import { statSync, unlink } from "node:fs";
10
+ import { join, basename, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { homedir } from "node:os";
13
+
14
+ const CONFIG_DIR = join(homedir(), ".colamd-themes");
15
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
16
+
17
+ /** Maximum allowed CSS file size (1MB) to prevent OOM attacks */
18
+ const MAX_CSS_SIZE = 1024 * 1024;
19
+
20
+ /** Built-in themes provided by @bytechain.cn/colamd */
21
+ export const BUILTIN_THEMES = ["light", "dark", "elegant", "newsprint"] as const;
22
+ export type BuiltinTheme = (typeof BUILTIN_THEMES)[number];
23
+
24
+ export interface ThemeConfig {
25
+ /** Default theme name (built-in or custom) */
26
+ defaultTheme: string;
27
+ /** Registered custom themes: name → CSS file path */
28
+ themes: Record<string, string>;
29
+ }
30
+
31
+ async function readConfig(): Promise<ThemeConfig> {
32
+ try {
33
+ await access(CONFIG_FILE);
34
+ const data = await readFile(CONFIG_FILE, "utf-8");
35
+ return JSON.parse(data) as ThemeConfig;
36
+ } catch {
37
+ return { defaultTheme: "elegant", themes: {} };
38
+ }
39
+ }
40
+
41
+ async function writeConfig(config: ThemeConfig): Promise<void> {
42
+ try {
43
+ await access(CONFIG_DIR);
44
+ } catch {
45
+ await mkdir(CONFIG_DIR, { recursive: true });
46
+ }
47
+ const tmpPath = `${CONFIG_FILE}.${process.pid}.${Date.now()}.tmp`;
48
+ await writeFile(tmpPath, JSON.stringify(config, null, 2), "utf-8");
49
+ await rename(tmpPath, CONFIG_FILE);
50
+ }
51
+
52
+ /** Get the full config. */
53
+ export async function getConfig(): Promise<ThemeConfig> {
54
+ return readConfig();
55
+ }
56
+
57
+ /** Set the default theme (built-in name or registered custom name). */
58
+ export async function setDefaultTheme(name: string): Promise<void> {
59
+ const config = await readConfig();
60
+ config.defaultTheme = name;
61
+ await writeConfig(config);
62
+ }
63
+
64
+ /**
65
+ * Register a custom theme.
66
+ * @param name Unique name (will be prefixed with "custom:" when applied)
67
+ * @param cssPath Absolute path to the CSS file
68
+ */
69
+ export async function registerTheme(name: string, cssPath: string): Promise<void> {
70
+ const config = await readConfig();
71
+ config.themes[name] = cssPath;
72
+ await writeConfig(config);
73
+ }
74
+
75
+ /** Remove a registered custom theme. */
76
+ export async function unregisterTheme(name: string): Promise<boolean> {
77
+ const config = await readConfig();
78
+ if (!(name in config.themes)) return false;
79
+ delete config.themes[name];
80
+ await writeConfig(config);
81
+ return true;
82
+ }
83
+
84
+ /**
85
+ * Sanitize theme name from file path.
86
+ * Removes special characters and limits length to prevent injection.
87
+ */
88
+ function sanitizeName(path: string): string {
89
+ return basename(path, ".css")
90
+ .replace(/[^a-zA-Z0-9_-]/g, "-")
91
+ .replace(/-+/g, "-")
92
+ .slice(0, 50) || "custom";
93
+ }
94
+
95
+ /**
96
+ * Validate CSS file before reading.
97
+ * Checks extension, size, and basic content validity.
98
+ */
99
+ async function validateCssFile(filePath: string): Promise<void> {
100
+ // Check file extension
101
+ if (!filePath.toLowerCase().endsWith(".css")) {
102
+ throw new Error(`Theme file must have .css extension: ${filePath}`);
103
+ }
104
+
105
+ // Check file size (synchronous stat is acceptable for metadata)
106
+ let stats;
107
+ try {
108
+ stats = statSync(filePath);
109
+ } catch {
110
+ throw new Error(`Cannot access theme file: ${filePath}`);
111
+ }
112
+
113
+ if (stats.size > MAX_CSS_SIZE) {
114
+ throw new Error(
115
+ `Theme file too large (${(stats.size / 1024).toFixed(1)}KB). ` +
116
+ `Maximum size: ${(MAX_CSS_SIZE / 1024).toFixed(0)}MB`
117
+ );
118
+ }
119
+
120
+ // Read and validate content
121
+ const content = await readFile(filePath, "utf-8");
122
+ if (content.length < 10) {
123
+ throw new Error(`Theme file appears to be empty or invalid: ${filePath}`);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Resolve a theme name to the arguments needed by the ColaMD renderer.
129
+ *
130
+ * @param name Theme name. If omitted, uses the configured default.
131
+ * @returns { themeName, customCSS? } — themeName is either a built-in name
132
+ * or "custom:<name>"; customCSS is the CSS content for custom themes.
133
+ */
134
+ export async function resolveTheme(name?: string): Promise<{
135
+ themeName: string;
136
+ customCSS?: string;
137
+ }> {
138
+ const config = await readConfig();
139
+ const resolved = name ?? config.defaultTheme;
140
+
141
+ // Check if it's a registered custom theme
142
+ if (config.themes[resolved]) {
143
+ const cssPath = config.themes[resolved];
144
+ await validateCssFile(cssPath);
145
+ const customCSS = await readFile(cssPath, "utf-8");
146
+ return { themeName: `custom:${resolved}`, customCSS };
147
+ }
148
+
149
+ // Check if it's a built-in theme
150
+ if (BUILTIN_THEMES.includes(resolved as BuiltinTheme)) {
151
+ // Prefer our 2.7.8-rc.1 contract CSS when a local theme file exists — our
152
+ // variant of a built-in name (e.g. elegant) wins over the renderer's own
153
+ // palette. applyTheme(name, css) takes over completely.
154
+ for (const base of [process.cwd(), join(dirname(fileURLToPath(import.meta.url)), "..")]) {
155
+ const cssPath = join(base, "themes", `${resolved}.css`);
156
+ try {
157
+ await validateCssFile(cssPath);
158
+ return { themeName: resolved, customCSS: await readFile(cssPath, "utf-8") };
159
+ } catch {
160
+ // No local contract CSS file for this built-in — fall back to the renderer's own.
161
+ }
162
+ }
163
+ return { themeName: resolved };
164
+ }
165
+
166
+ // Treat as a direct CSS file path
167
+ try {
168
+ await access(resolved);
169
+ } catch {
170
+ throw new Error(
171
+ `Unknown theme: "${resolved}". Use a built-in theme (${BUILTIN_THEMES.join(", ")}), ` +
172
+ `a registered custom theme, or a path to a .css file.`
173
+ );
174
+ }
175
+
176
+ // Validate and read direct CSS file
177
+ await validateCssFile(resolved);
178
+ const nameFromPath = sanitizeName(resolved);
179
+ const customCSS = await readFile(resolved, "utf-8");
180
+
181
+ return {
182
+ themeName: `custom:${nameFromPath}`,
183
+ customCSS,
184
+ };
185
+ }
@@ -1,12 +1,12 @@
1
- declare module "pdf-parse" {
2
- interface PDFData {
3
- text: string;
4
- numpages: number;
5
- info: Record<string, unknown>;
6
- metadata: Record<string, unknown>;
7
- version: string;
8
- }
9
-
10
- function pdfParse(buffer: Buffer): Promise<PDFData>;
11
- export default pdfParse;
12
- }
1
+ declare module "pdf-parse" {
2
+ interface PDFData {
3
+ text: string;
4
+ numpages: number;
5
+ info: Record<string, unknown>;
6
+ metadata: Record<string, unknown>;
7
+ version: string;
8
+ }
9
+
10
+ function pdfParse(buffer: Buffer): Promise<PDFData>;
11
+ export default pdfParse;
12
+ }
@@ -1,84 +1,84 @@
1
- /**
2
- * Shared utility functions for CLI commands.
3
- * Provides consistent file path resolution and error handling.
4
- */
5
-
6
- import { existsSync } from "node:fs";
7
- import { resolve, isAbsolute, basename } from "node:path";
8
-
9
- /**
10
- * Resolve input file path with validation.
11
- * Converts relative paths to absolute and checks existence.
12
- *
13
- * @param input File path provided by user
14
- * @returns Resolved absolute path
15
- * @throws Error if file does not exist
16
- */
17
- export function resolveInputPath(input: string): string {
18
- const path = isAbsolute(input) ? input : resolve(process.cwd(), input);
19
- if (!existsSync(path)) {
20
- throw new Error(`File not found: ${path}`);
21
- }
22
- return path;
23
- }
24
-
25
- /**
26
- * Resolve output file path with sensible defaults.
27
- * If output path is not specified, derives it from input path by replacing extension.
28
- *
29
- * @param inputPath Resolved absolute input path
30
- * @param output Optional explicit output path
31
- * @param ext Target extension (default: ".html")
32
- * @returns Resolved absolute output path
33
- */
34
- export function resolveOutputPath(inputPath: string, output?: string, ext = ".html"): string {
35
- if (output) {
36
- return isAbsolute(output) ? output : resolve(process.cwd(), output);
37
- }
38
- return inputPath.replace(/\.md$/i, ext);
39
- }
40
-
41
- /**
42
- * Get base filename without extension for use as document title.
43
- *
44
- * @param filePath Absolute or relative file path
45
- * @returns Filename without extension
46
- */
47
- export function getDocumentTitle(filePath: string): string {
48
- return basename(filePath, ".md");
49
- }
50
-
51
- /**
52
- * Standard error codes for CLI operations.
53
- * Enables consistent error handling across all commands.
54
- */
55
- export enum ErrorCode {
56
- /** Input file not found */
57
- FILE_NOT_FOUND = 1,
58
- /** Theme resolution failed */
59
- THEME_ERROR = 2,
60
- /** Rendering/export failed */
61
- RENDER_ERROR = 3,
62
- /** Invalid arguments or configuration */
63
- INVALID_INPUT = 4,
64
- /** Unknown/unexpected error */
65
- UNKNOWN = 99,
66
- }
67
-
68
- /**
69
- * Custom error class for CLI operations with structured error codes.
70
- * Replaces direct process.exit() calls for better error propagation.
71
- */
72
- export class CLIError extends Error {
73
- constructor(
74
- message: string,
75
- public code: ErrorCode = ErrorCode.UNKNOWN,
76
- public cause?: Error
77
- ) {
78
- super(message);
79
- this.name = "CLIError";
80
- if (cause) {
81
- this.stack = `${this.message}\nCaused by: ${cause.stack}`;
82
- }
83
- }
84
- }
1
+ /**
2
+ * Shared utility functions for CLI commands.
3
+ * Provides consistent file path resolution and error handling.
4
+ */
5
+
6
+ import { existsSync } from "node:fs";
7
+ import { resolve, isAbsolute, basename } from "node:path";
8
+
9
+ /**
10
+ * Resolve input file path with validation.
11
+ * Converts relative paths to absolute and checks existence.
12
+ *
13
+ * @param input File path provided by user
14
+ * @returns Resolved absolute path
15
+ * @throws Error if file does not exist
16
+ */
17
+ export function resolveInputPath(input: string): string {
18
+ const path = isAbsolute(input) ? input : resolve(process.cwd(), input);
19
+ if (!existsSync(path)) {
20
+ throw new Error(`File not found: ${path}`);
21
+ }
22
+ return path;
23
+ }
24
+
25
+ /**
26
+ * Resolve output file path with sensible defaults.
27
+ * If output path is not specified, derives it from input path by replacing extension.
28
+ *
29
+ * @param inputPath Resolved absolute input path
30
+ * @param output Optional explicit output path
31
+ * @param ext Target extension (default: ".html")
32
+ * @returns Resolved absolute output path
33
+ */
34
+ export function resolveOutputPath(inputPath: string, output?: string, ext = ".html"): string {
35
+ if (output) {
36
+ return isAbsolute(output) ? output : resolve(process.cwd(), output);
37
+ }
38
+ return inputPath.replace(/\.md$/i, ext);
39
+ }
40
+
41
+ /**
42
+ * Get base filename without extension for use as document title.
43
+ *
44
+ * @param filePath Absolute or relative file path
45
+ * @returns Filename without extension
46
+ */
47
+ export function getDocumentTitle(filePath: string): string {
48
+ return basename(filePath, ".md");
49
+ }
50
+
51
+ /**
52
+ * Standard error codes for CLI operations.
53
+ * Enables consistent error handling across all commands.
54
+ */
55
+ export enum ErrorCode {
56
+ /** Input file not found */
57
+ FILE_NOT_FOUND = 1,
58
+ /** Theme resolution failed */
59
+ THEME_ERROR = 2,
60
+ /** Rendering/export failed */
61
+ RENDER_ERROR = 3,
62
+ /** Invalid arguments or configuration */
63
+ INVALID_INPUT = 4,
64
+ /** Unknown/unexpected error */
65
+ UNKNOWN = 99,
66
+ }
67
+
68
+ /**
69
+ * Custom error class for CLI operations with structured error codes.
70
+ * Replaces direct process.exit() calls for better error propagation.
71
+ */
72
+ export class CLIError extends Error {
73
+ constructor(
74
+ message: string,
75
+ public code: ErrorCode = ErrorCode.UNKNOWN,
76
+ public cause?: Error
77
+ ) {
78
+ super(message);
79
+ this.name = "CLIError";
80
+ if (cause) {
81
+ this.stack = `${this.message}\nCaused by: ${cause.stack}`;
82
+ }
83
+ }
84
+ }