@wenyan-md/cli 1.0.9 → 1.0.10

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,147 @@
1
+ <div align="center">
2
+ <img alt="logo" src="https://raw.githubusercontent.com/caol64/wenyan/main/Data/256-mac.png" width="120" />
3
+ </div>
4
+
5
+ # wenyan-cli (Docker)
6
+
7
+ **Render Markdown to beautifully styled articles and publish to content platforms — powered by Docker.**
8
+
9
+ > This image bundles **wenyan CLI** and all required runtime dependencies.
10
+ > No local Node.js or npm environment required.
11
+
12
+ ## Quick Start
13
+
14
+ ### Pull image
15
+
16
+ ```bash
17
+ docker pull caol64/wenyan-cli
18
+ ```
19
+
20
+ ### Show help
21
+
22
+ ```bash
23
+ docker run --rm caol64/wenyan-cli --help
24
+ ```
25
+
26
+ ## Basic Usage
27
+
28
+ Render and publish a Markdown file to WeChat Official Account draft box:
29
+
30
+ ```bash
31
+ docker run --rm \
32
+ --env-file .env \
33
+ -e HOST_FILE_PATH=$(pwd) \
34
+ -v $(pwd):/mnt/host-downloads \
35
+ caol64/wenyan-cli \
36
+ publish -f ./article.md
37
+ ```
38
+
39
+ Render Markdown content directly:
40
+
41
+ ```bash
42
+ docker run --rm caol64/wenyan-cli \
43
+ render "# Hello Wenyan"
44
+ ```
45
+
46
+ ## Working with Local Files (Recommended)
47
+
48
+ When using local Markdown or image files, mount the current directory:
49
+
50
+ ```bash
51
+ docker run --rm \
52
+ -e HOST_FILE_PATH=$(pwd) \
53
+ -v $(pwd):/mnt/host-downloads \
54
+ caol64/wenyan-cli \
55
+ publish -f ./example.md
56
+ ```
57
+
58
+ **How it works:**
59
+
60
+ | Path | Description |
61
+ | --------------------- | ----------------------------- |
62
+ | `HOST_FILE_PATH` | Absolute path on host machine |
63
+ | `/mnt/host-downloads` | Mounted path inside container |
64
+
65
+ All file paths in Markdown (cover / images) should reference host paths.
66
+
67
+ ## Input Methods
68
+
69
+ `publish` supports **exactly one** input source:
70
+
71
+ - `-f <file>` — read Markdown from local file
72
+ - `<input-content>` — inline Markdown string
73
+ - `stdin` — pipe from another command
74
+
75
+ Examples:
76
+
77
+ ```bash
78
+ cat article.md | docker run --rm -i caol64/wenyan-cli render
79
+ ```
80
+
81
+ ```bash
82
+ docker run --rm caol64/wenyan-cli render "# Title"
83
+ ```
84
+
85
+ ## Options
86
+
87
+ Commonly used options:
88
+
89
+ - `-t, --theme` — theme ID (default: `default`)
90
+ - `-h, --highlight` — code highlight theme
91
+ - `--no-mac-style` — disable macOS-style code blocks
92
+ - `--no-footnote` — disable link-to-footnote conversion
93
+
94
+ ## Markdown Frontmatter (Required)
95
+
96
+ Each Markdown file must include frontmatter:
97
+
98
+ ```md
99
+ ---
100
+ title: My Article Title
101
+ cover: /absolute/path/to/cover.jpg
102
+ ---
103
+ ```
104
+
105
+ - `title` — article title (required)
106
+ - `cover` — optional cover image (local or remote)
107
+
108
+ ## Environment Variables
109
+
110
+ Publishing to WeChat requires:
111
+
112
+ - `WECHAT_APP_ID`
113
+ - `WECHAT_APP_SECRET`
114
+
115
+ Recommended usage with `.env` file:
116
+
117
+ ```env
118
+ WECHAT_APP_ID=xxx
119
+ WECHAT_APP_SECRET=yyy
120
+ ```
121
+
122
+ ## Image Details
123
+
124
+ - Entrypoint: `wenyan`
125
+ - Runtime: Node.js (bundled)
126
+ - Architecture: `linux/amd64`, `linux/arm64`
127
+
128
+ ## License
129
+
130
+ Apache License Version 2.0
131
+
132
+ ### Tip
133
+
134
+ For frequent usage, create an alias:
135
+
136
+ ```bash
137
+ alias wenyan='docker run --rm \
138
+ -e HOST_FILE_PATH=$(pwd) \
139
+ -v $(pwd):/mnt/host-downloads \
140
+ caol64/wenyan-cli'
141
+ ```
142
+
143
+ Then use it like a native CLI:
144
+
145
+ ```bash
146
+ wenyan publish -f article.md
147
+ ```
package/README.md CHANGED
@@ -7,11 +7,12 @@
7
7
  [![npm](https://img.shields.io/npm/v/@wenyan-md/cli)](https://www.npmjs.com/package/@wenyan-md/cli)
8
8
  [![License](https://img.shields.io/github/license/caol64/wenyan-cli)](LICENSE)
9
9
  ![NPM Downloads](https://img.shields.io/npm/dm/%40wenyan-md%2Fcli)
10
+ [![Docker Pulls](https://img.shields.io/docker/pulls/caol64/wenyan-cli)](https://hub.docker.com/r/caol64/wenyan-cli)
10
11
  [![Stars](https://img.shields.io/github/stars/caol64/wenyan-cli?style=social)](https://github.com/caol64/wenyan-cli)
11
12
 
12
13
  ## 简介
13
14
 
14
- **文颜(Wenyan)** 是一款多平台 Markdown 排版与发布工具,支持将 Markdown 一键转换并发布至:
15
+ **[文颜(Wenyan)](https://wenyan.yuzhi.tech)** 是一款多平台 Markdown 排版与发布工具,支持将 Markdown 一键转换并发布至:
15
16
 
16
17
  - 微信公众号
17
18
  - 知乎
@@ -20,13 +21,6 @@
20
21
 
21
22
  文颜的目标是:**让写作者专注内容,而不是排版和平台适配**。
22
23
 
23
- 本仓库是 **文颜的 CLI 版本**,适合以下场景:
24
-
25
- - 命令行使用
26
- - CI / CD 自动化发布
27
- - 脚本或工具链集成
28
- - 与 AI / MCP 系统联动自动发文
29
-
30
24
  ## 文颜的不同版本
31
25
 
32
26
  文颜目前提供多种形态,覆盖不同使用场景:
@@ -37,28 +31,6 @@
37
31
  - [MCP 版本](https://github.com/caol64/wenyan-mcp) - AI 自动发文
38
32
  - [核心库](https://github.com/caol64/wenyan-core) - 嵌入 Node / Web 项目
39
33
 
40
- ## 功能特性
41
-
42
- - 使用内置主题对 Markdown 内容排版
43
- - 自动处理并上传图片(本地 / 网络)
44
- - 支持数学公式(MathJax)
45
- - 一键发布文章到微信公众号草稿箱
46
- - 支持 CI / 自动化流程调用
47
-
48
- ## 主题效果预览
49
-
50
- 👉 [内置主题预览](https://yuzhi.tech/docs/wenyan/theme)
51
-
52
- 文颜内置并适配了多个优秀的 Typora 主题,在此感谢原作者:
53
-
54
- - [Orange Heart](https://github.com/evgo2017/typora-theme-orange-heart)
55
- - [Rainbow](https://github.com/thezbm/typora-theme-rainbow)
56
- - [Lapis](https://github.com/YiNNx/typora-theme-lapis)
57
- - [Pie](https://github.com/kevinzhao2233/typora-theme-pie)
58
- - [Maize](https://github.com/BEATREE/typora-maize-theme)
59
- - [Purple](https://github.com/hliu202/typora-purple-theme)
60
- - [物理猫-薄荷](https://github.com/sumruler/typora-theme-phycat)
61
-
62
34
  ## 安装方式
63
35
 
64
36
  ### 方式一:npm(推荐)
@@ -114,51 +86,18 @@ CLI 主命令:
114
86
  wenyan <command> [options]
115
87
  ```
116
88
 
117
- 目前最常用的子命令是 `publish`。
118
-
119
- ## 环境变量配置
120
-
121
- 部分功能(如发布微信公众号)需要配置以下环境变量:
122
-
123
- - `WECHAT_APP_ID`
124
- - `WECHAT_APP_SECRET`
125
-
126
- ### macOS / Linux
127
-
128
- 临时使用:
129
-
130
- ```bash
131
- WECHAT_APP_ID=xxx WECHAT_APP_SECRET=yyy wenyan publish "your markdown"
132
- ```
133
-
134
- 永久配置(推荐):
135
-
136
- ```bash
137
- export WECHAT_APP_ID=xxx
138
- export WECHAT_APP_SECRET=yyy
139
- ```
140
-
141
- ### Windows (PowerShell)
142
-
143
- 临时使用:
144
-
145
- ```powershell
146
- $env:WECHAT_APP_ID="xxx"
147
- $env:WECHAT_APP_SECRET="yyy"
148
- wenyan publish example.md
149
- ```
150
-
151
- 永久设置(在环境变量里添加):
152
-
153
- 控制面板 → 系统和安全 → 系统 → 高级系统设置 → 环境变量 → 添加 `WECHAT_APP_ID` 和 `WECHAT_APP_SECRET`。
89
+ 目前支持的子命令有
90
+ - `publish` 排版并发布到公众号草稿箱
91
+ - `render` 仅排版,用做测试
92
+ - `theme` 主题管理
154
93
 
155
94
  ## 子命令
156
95
 
157
- `publish`
96
+ ### `publish`
158
97
 
159
98
  将 Markdown 转换为适配微信公众号的富文本 HTML,并上传到草稿箱。
160
99
 
161
- ### 参数
100
+ #### 参数
162
101
 
163
102
  - `<input-content>`
164
103
 
@@ -167,17 +106,18 @@ wenyan publish example.md
167
106
  - 直接作为参数传入
168
107
  - 通过 stdin 管道输入
169
108
 
170
- ### 常用选项
109
+ #### 常用选项
171
110
 
172
- - `-t`:主题(默认 `default`)
173
- - default / orangeheart / rainbow / lapis / pie / maize / purple / phycat
174
- - `-h`:代码高亮主题(默认 `solarized-light`)
111
+ - `-t <theme-id>`:主题id(默认 `default`),可以是内置主题,也可以是通过`theme --add`添加的自定义主题
112
+ - [内置主题](https://github.com/caol64/wenyan-core/tree/main/src/assets/themes)
113
+ - `-h <highlight-theme-id>`:代码高亮主题(默认 `solarized-light`)
175
114
  - atom-one-dark / atom-one-light / dracula / github-dark / github / monokai / solarized-dark / solarized-light / xcode
176
115
  - `--no-mac-style`:关闭代码块 Mac 风格
177
116
  - `--no-footnote`:关闭链接转脚注
178
- - `-f`:指定本地 Markdown 文件路径
117
+ - `-f <path>`:指定本地 Markdown 文件路径
118
+ - `-c <path>`:指定临时自定义主题路径,优先级大于`-t`
179
119
 
180
- ## 使用示例
120
+ #### 使用示例
181
121
 
182
122
  直接传入内容:
183
123
 
@@ -197,23 +137,60 @@ cat example.md | wenyan publish -t lapis -h solarized-light --no-mac-style
197
137
  wenyan publish -f "./example.md" -t lapis -h solarized-light --no-mac-style
198
138
  ```
199
139
 
200
- ## Markdown Frontmatter 说明(必读)
140
+ ### `theme`
201
141
 
202
- 为了正确上传文章,每篇 Markdown 顶部需要包含 frontmatter:
142
+ 主题管理,浏览内置主题、添加/删除自定义主题。
203
143
 
204
- ```md
205
- ---
206
- title: 在本地跑一个大语言模型(2) - 给模型提供外部知识库
207
- cover: /Users/xxx/image.jpg
208
- ---
144
+ #### 参数
145
+
146
+ 无。
147
+
148
+ #### 常用选项
149
+
150
+ - `-l`:列出所有可用主题
151
+ - `--add`:添加自定义主题(永久)
152
+ - `--name <name>`:主题名称
153
+ - `--path <path>`:主题路径(本地或网络)
154
+ - `--rm <name>`:删除自定义主题
155
+
156
+ #### 使用示例
157
+
158
+ 列出可用主题:
159
+
160
+ ```bash
161
+ wenyan theme -l
209
162
  ```
210
163
 
211
- 字段说明:
164
+ 安装自定义主题
212
165
 
213
- - `title` 文章标题(必填)
214
- - `cover` 文章封面
215
- - 本地路径或网络图片
216
- - 如果正文中已有图片,可省略
166
+ ```bash
167
+ wenyan theme --add --name new-theme --path https://wenyan.yuzhi.tech/manhua.css
168
+ ```
169
+
170
+ 删除自定义主题
171
+
172
+ ```bash
173
+ wenyan theme --rm new-theme
174
+ ```
175
+
176
+ ## 使用自定义主题
177
+
178
+ 你可以通过两种途径使用自定义主题:
179
+
180
+ - 不安装直接使用
181
+
182
+ ```bash
183
+ wenyan publish -f "./example.md" -c "/path/to/theme" -h solarized-light --no-mac-style
184
+ ```
185
+
186
+ - 先安装再使用:
187
+
188
+ ```bash
189
+ wenyan theme --add --name new-theme --path https://wenyan.yuzhi.tech/manhua.css
190
+ wenyan publish -f "./example.md" -t new-theme -h solarized-light --no-mac-style
191
+ ```
192
+
193
+ 区别在于,安装后的主题永久有效。
217
194
 
218
195
  ## 关于图片自动上传
219
196
 
@@ -222,14 +199,68 @@ cover: /Users/xxx/image.jpg
222
199
  - 本地路径(如:`/Users/xxx/image.jpg`)
223
200
  - 网络路径(如:`https://example.com/image.jpg`)
224
201
 
202
+ ## 环境变量配置
203
+
204
+ 部分功能(如发布微信公众号)需要配置以下环境变量:
205
+
206
+ - `WECHAT_APP_ID`
207
+ - `WECHAT_APP_SECRET`
208
+
209
+ ### macOS / Linux
210
+
211
+ 临时使用:
212
+
213
+ ```bash
214
+ WECHAT_APP_ID=xxx WECHAT_APP_SECRET=yyy wenyan publish "your markdown"
215
+ ```
216
+
217
+ 永久配置(推荐):
218
+
219
+ ```bash
220
+ export WECHAT_APP_ID=xxx
221
+ export WECHAT_APP_SECRET=yyy
222
+ ```
223
+
224
+ ### Windows (PowerShell)
225
+
226
+ 临时使用:
227
+
228
+ ```powershell
229
+ $env:WECHAT_APP_ID="xxx"
230
+ $env:WECHAT_APP_SECRET="yyy"
231
+ wenyan publish example.md
232
+ ```
233
+
234
+ 永久设置(在环境变量里添加):
235
+
236
+ 控制面板 → 系统和安全 → 系统 → 高级系统设置 → 环境变量 → 添加 `WECHAT_APP_ID` 和 `WECHAT_APP_SECRET`。
237
+
225
238
  ## 微信公众号 IP 白名单
226
239
 
227
- > ⚠️ 重要
240
+ > [!IMPORTANT]
228
241
  >
229
242
  > 请确保运行文颜的机器 IP 已加入微信公众号后台的 IP 白名单,否则上传接口将调用失败。
230
243
 
231
244
  配置说明文档:[https://yuzhi.tech/docs/wenyan/upload](https://yuzhi.tech/docs/wenyan/upload)
232
245
 
246
+ ## Markdown Frontmatter 说明(必读)
247
+
248
+ 为了正确上传文章,每篇 Markdown 顶部需要包含 frontmatter:
249
+
250
+ ```md
251
+ ---
252
+ title: 在本地跑一个大语言模型(2) - 给模型提供外部知识库
253
+ cover: /Users/xxx/image.jpg
254
+ ---
255
+ ```
256
+
257
+ 字段说明:
258
+
259
+ - `title` 文章标题(必填)
260
+ - `cover` 文章封面
261
+ - 本地路径或网络图片
262
+ - 如果正文中已有图片,可省略
263
+
233
264
  ## 示例文章格式
234
265
 
235
266
  ```md
package/dist/cli.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { createProgram } from "./index.js";
3
+ const program = createProgram();
4
+ program.parse(process.argv);
@@ -1,35 +1,12 @@
1
- import { getGzhContent } from "@wenyan-md/core/wrapper";
2
1
  import { publishToDraft } from "@wenyan-md/core/publish";
3
- import { getNormalizeFilePath, readStdin } from "../utils.js";
4
- import fs from "node:fs/promises";
5
- import path from "node:path";
2
+ import { prepareRenderContext, runCommandWrapper } from "./render.js";
6
3
  export async function publishCommand(inputContent, options) {
7
- try {
8
- const { file } = options;
9
- let absoluteDirPath = undefined;
10
- if (!inputContent) {
11
- if (!process.stdin.isTTY) {
12
- inputContent = await readStdin();
13
- }
14
- }
15
- if (!inputContent && file) {
16
- const normalizePath = getNormalizeFilePath(file);
17
- inputContent = await fs.readFile(normalizePath, "utf-8");
18
- absoluteDirPath = path.dirname(normalizePath);
19
- }
20
- if (!inputContent) {
21
- console.error("Error: missing input-content (no argument, no stdin, and no file).");
22
- process.exit(1);
23
- }
24
- const gzhContent = await getGzhContent(inputContent, options["theme"], options["highlight"], options["macStyle"], options["footnote"]);
25
- if (!gzhContent.title) {
26
- console.error("未能找到文章标题");
27
- process.exit(1);
28
- }
29
- if (!gzhContent.cover) {
30
- console.error("未能找到文章封面");
31
- process.exit(1);
32
- }
4
+ await runCommandWrapper(async () => {
5
+ const { gzhContent, absoluteDirPath } = await prepareRenderContext(inputContent, options);
6
+ if (!gzhContent.title)
7
+ throw new Error("Error: 未能找到文章标题");
8
+ if (!gzhContent.cover)
9
+ throw new Error("Error: 未能找到文章封面");
33
10
  const data = await publishToDraft(gzhContent.title, gzhContent.content, gzhContent.cover, {
34
11
  relativePath: absoluteDirPath,
35
12
  });
@@ -38,16 +15,7 @@ export async function publishCommand(inputContent, options) {
38
15
  }
39
16
  else {
40
17
  console.error(`上传失败,\n${data}`);
18
+ process.exit(1);
41
19
  }
42
- }
43
- catch (error) {
44
- if (error instanceof Error) {
45
- console.error("An unexpected error occurred during publishing:");
46
- console.error(error.message);
47
- }
48
- else {
49
- console.error("An unexpected error occurred:", error);
50
- }
51
- process.exit(1);
52
- }
20
+ });
53
21
  }
@@ -1,30 +1,61 @@
1
- import { getGzhContent } from "@wenyan-md/core/wrapper";
1
+ import { configStore, renderStyledContent } from "@wenyan-md/core/wrapper";
2
2
  import { getNormalizeFilePath, readStdin } from "../utils.js";
3
3
  import fs from "node:fs/promises";
4
- export async function renderCommand(inputContent, options) {
4
+ import path from "node:path";
5
+ // --- 处理输入源、文件路径和主题 ---
6
+ export async function prepareRenderContext(inputContent, options) {
7
+ const { file, theme, customTheme, highlight, macStyle, footnote } = options;
8
+ let absoluteDirPath = undefined;
9
+ // 1. 尝试从 Stdin 读取
10
+ if (!inputContent && !process.stdin.isTTY) {
11
+ inputContent = await readStdin();
12
+ }
13
+ // 2. 尝试从文件读取
14
+ if (!inputContent && file) {
15
+ const normalizePath = getNormalizeFilePath(file);
16
+ inputContent = await fs.readFile(normalizePath, "utf-8");
17
+ absoluteDirPath = path.dirname(normalizePath);
18
+ }
19
+ // 3. 校验输入
20
+ if (!inputContent) {
21
+ throw new Error("Error: missing input-content (no argument, no stdin, and no file).");
22
+ }
23
+ let handledCustomTheme = customTheme;
24
+ // 4. 当用户传入自定义主题路径时,优先级最高
25
+ if (customTheme) {
26
+ const normalizePath = getNormalizeFilePath(customTheme);
27
+ handledCustomTheme = await fs.readFile(normalizePath, "utf-8");
28
+ }
29
+ else if (theme) {
30
+ // 否则尝试读取配置中的自定义主题
31
+ handledCustomTheme = configStore.getThemeById(theme);
32
+ }
33
+ if (!handledCustomTheme && !theme) {
34
+ throw new Error(`Error: theme "${theme}" not found.`);
35
+ }
36
+ // 5. 执行核心渲染
37
+ const gzhContent = await renderStyledContent(inputContent, {
38
+ themeId: theme,
39
+ hlThemeId: highlight,
40
+ isMacStyle: macStyle,
41
+ isAddFootnote: footnote,
42
+ themeCss: handledCustomTheme,
43
+ });
44
+ return { gzhContent, absoluteDirPath };
45
+ }
46
+ // --- 统一的错误处理包装器 ---
47
+ export async function runCommandWrapper(action) {
5
48
  try {
6
- const { file } = options;
7
- if (!inputContent) {
8
- if (!process.stdin.isTTY) {
9
- inputContent = await readStdin();
10
- }
11
- }
12
- if (!inputContent && file) {
13
- const normalizePath = getNormalizeFilePath(file);
14
- inputContent = await fs.readFile(normalizePath, "utf-8");
15
- }
16
- if (!inputContent) {
17
- console.error("Error: missing input-content (no argument, no stdin, and no file).");
18
- process.exit(1);
19
- }
20
- const gzhContent = await getGzhContent(inputContent, options["theme"], options["highlight"], options["macStyle"], options["footnote"]);
21
- console.log(gzhContent.content);
22
- // process.exit(0);
49
+ await action();
23
50
  }
24
51
  catch (error) {
25
52
  if (error instanceof Error) {
26
- console.error("An unexpected error occurred during publishing:");
27
- console.error(error.message);
53
+ if (error.message.startsWith("Error:")) {
54
+ console.error(error.message);
55
+ }
56
+ else {
57
+ console.error("An unexpected error occurred:", error.message);
58
+ }
28
59
  }
29
60
  else {
30
61
  console.error("An unexpected error occurred:", error);
@@ -32,3 +63,9 @@ export async function renderCommand(inputContent, options) {
32
63
  process.exit(1);
33
64
  }
34
65
  }
66
+ export async function renderCommand(inputContent, options) {
67
+ await runCommandWrapper(async () => {
68
+ const { gzhContent } = await prepareRenderContext(inputContent, options);
69
+ console.log(gzhContent.content);
70
+ });
71
+ }
@@ -0,0 +1,78 @@
1
+ import { getAllGzhThemes } from "@wenyan-md/core";
2
+ import { getNormalizeFilePath } from "../utils.js";
3
+ import fs from "node:fs/promises";
4
+ import { configStore } from "@wenyan-md/core/wrapper";
5
+ export async function themeCommand(options) {
6
+ const { list, add, name, path, rm } = options;
7
+ if (list) {
8
+ listThemes();
9
+ return;
10
+ }
11
+ if (add) {
12
+ await addTheme(name, path);
13
+ return;
14
+ }
15
+ if (rm) {
16
+ await removeTheme(rm);
17
+ return;
18
+ }
19
+ }
20
+ function listThemes() {
21
+ const themes = getAllGzhThemes();
22
+ console.log("\n内置主题:");
23
+ themes.forEach((theme) => console.log(`- ${theme.meta.id}: ${theme.meta.description}`));
24
+ const customThemes = configStore.getThemes();
25
+ if (customThemes.length > 0) {
26
+ console.log("\n自定义主题:");
27
+ customThemes.forEach((theme) => {
28
+ console.log(`- ${theme.id}: ${theme.description ?? ""}`);
29
+ });
30
+ }
31
+ console.log("");
32
+ }
33
+ async function addTheme(name, path) {
34
+ if (!name || !path) {
35
+ console.log("❌ 添加主题时必须提供名称(name)和路径(path)\n");
36
+ return;
37
+ }
38
+ if (checkThemeExists(name) || checkCustomThemeExists(name)) {
39
+ console.log(`❌ 主题 "${name}" 已存在\n`);
40
+ return;
41
+ }
42
+ if (path.startsWith("http")) {
43
+ console.log(`⏳ 正在从远程获取主题: ${path} ...`);
44
+ const response = await fetch(path);
45
+ if (!response.ok) {
46
+ console.log(`❌ 无法从远程获取主题: ${response.statusText}\n`);
47
+ return;
48
+ }
49
+ const content = await response.text();
50
+ configStore.addThemeToConfig(name, content);
51
+ }
52
+ else {
53
+ const normalizePath = getNormalizeFilePath(path);
54
+ const content = await fs.readFile(normalizePath, "utf-8");
55
+ configStore.addThemeToConfig(name, content);
56
+ }
57
+ console.log(`✅ 主题 "${name}" 已添加\n`);
58
+ }
59
+ async function removeTheme(name) {
60
+ if (checkThemeExists(name)) {
61
+ console.log(`❌ 默认主题 "${name}" 不能删除\n`);
62
+ return;
63
+ }
64
+ if (!checkCustomThemeExists(name)) {
65
+ console.log(`❌ 自定义主题 "${name}" 不存在\n`);
66
+ return;
67
+ }
68
+ configStore.deleteThemeFromConfig(name);
69
+ console.log(`✅ 主题 "${name}" 已删除\n`);
70
+ }
71
+ function checkThemeExists(themeId) {
72
+ const themes = getAllGzhThemes();
73
+ return themes.some((theme) => theme.meta.id === themeId);
74
+ }
75
+ function checkCustomThemeExists(themeId) {
76
+ const customThemes = configStore.getThemes();
77
+ return customThemes.some((theme) => theme.id === themeId);
78
+ }
package/dist/index.js CHANGED
@@ -2,37 +2,42 @@ import { Command } from "commander";
2
2
  import { publishCommand } from "./commands/publish.js";
3
3
  import { renderCommand } from "./commands/render.js";
4
4
  import pkg from "../package.json" with { type: "json" };
5
- const program = new Command();
6
- program
7
- .name("wenyan")
8
- .description("A CLI for WenYan Markdown Render.")
9
- .version(pkg.version, "-v, --version", "output the current version")
10
- .action(() => {
11
- program.outputHelp();
12
- return;
13
- });
14
- program
15
- .command("publish")
16
- .description("Render a markdown file to styled HTML and publish to wechat GZH")
17
- .argument("[input-content]", "markdown content (string input)")
18
- .option("-f, --file <path>", "read markdown content from local file")
19
- .option("-t, --theme <theme-id>", "ID of the theme to use", "default")
20
- .option("-h, --highlight <highlight-theme-id>", "ID of the code highlight theme to use", "solarized-light")
21
- .option("--mac-style", "display codeblock with mac style", true)
22
- .option("--no-mac-style", "disable mac style")
23
- .option("--footnote", "convert link to footnote", true)
24
- .option("--no-footnote", "disable footnote")
25
- .action(publishCommand);
26
- program
27
- .command("render")
28
- .description("Render a markdown file to styled HTML")
29
- .argument("[input-content]", "markdown content (string input)")
30
- .option("-f, --file <path>", "read markdown content from local file")
31
- .option("-t, --theme <theme-id>", "ID of the theme to use", "default")
32
- .option("-h, --highlight <highlight-theme-id>", "ID of the code highlight theme to use", "solarized-light")
33
- .option("--mac-style", "display codeblock with mac style", true)
34
- .option("--no-mac-style", "disable mac style")
35
- .option("--footnote", "convert link to footnote", true)
36
- .option("--no-footnote", "disable footnote")
37
- .action(renderCommand);
38
- program.parse();
5
+ import { themeCommand } from "./commands/theme.js";
6
+ export function createProgram(version = pkg.version) {
7
+ const program = new Command();
8
+ program
9
+ .name("wenyan")
10
+ .description("A CLI for WenYan Markdown Render.")
11
+ .version(version, "-v, --version", "output the current version")
12
+ .action(() => {
13
+ program.outputHelp();
14
+ });
15
+ const addCommonOptions = (cmd) => {
16
+ return cmd
17
+ .argument("[input-content]", "markdown content (string input)")
18
+ .option("-f, --file <path>", "read markdown content from local file")
19
+ .option("-t, --theme <theme-id>", "ID of the theme to use", "default")
20
+ .option("-h, --highlight <highlight-theme-id>", "ID of the code highlight theme to use", "solarized-light")
21
+ .option("-c, --custom-theme <path>", "path to custom theme CSS file")
22
+ .option("--mac-style", "display codeblock with mac style", true)
23
+ .option("--no-mac-style", "disable mac style")
24
+ .option("--footnote", "convert link to footnote", true)
25
+ .option("--no-footnote", "disable footnote");
26
+ };
27
+ const pubCmd = program
28
+ .command("publish")
29
+ .description("Render a markdown file to styled HTML and publish to wechat GZH");
30
+ addCommonOptions(pubCmd).action(publishCommand);
31
+ const renderCmd = program.command("render").description("Render a markdown file to styled HTML");
32
+ addCommonOptions(renderCmd).action(renderCommand);
33
+ program
34
+ .command("theme")
35
+ .description("Manage themes")
36
+ .option("-l, --list", "List all available themes")
37
+ .option("--add", "Add a new custom theme")
38
+ .option("--name <name>", "Name of the new custom theme")
39
+ .option("--path <path>", "Path to the new custom theme CSS file")
40
+ .option("--rm <name>", "Name of the custom theme to remove")
41
+ .action(themeCommand);
42
+ return program;
43
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -1,9 +1,2 @@
1
- interface RenderOptions {
2
- file?: string;
3
- theme: string;
4
- highlight: string;
5
- macStyle: boolean;
6
- footnote: boolean;
7
- }
1
+ import { RenderOptions } from "../types.js";
8
2
  export declare function publishCommand(inputContent: string | undefined, options: RenderOptions): Promise<void>;
9
- export {};
@@ -1,9 +1,7 @@
1
- interface RenderOptions {
2
- file?: string;
3
- theme: string;
4
- highlight: string;
5
- macStyle: boolean;
6
- footnote: boolean;
7
- }
1
+ import { RenderOptions } from "../types.js";
2
+ export declare function prepareRenderContext(inputContent: string | undefined, options: RenderOptions): Promise<{
3
+ gzhContent: import("@wenyan-md/core/wrapper").StyledContent;
4
+ absoluteDirPath: string | undefined;
5
+ }>;
6
+ export declare function runCommandWrapper(action: () => Promise<void>): Promise<void>;
8
7
  export declare function renderCommand(inputContent: string | undefined, options: RenderOptions): Promise<void>;
9
- export {};
@@ -0,0 +1,2 @@
1
+ import { ThemeOptions } from "../types.js";
2
+ export declare function themeCommand(options: ThemeOptions): Promise<void>;
@@ -1 +1,2 @@
1
- export {};
1
+ import { Command } from "commander";
2
+ export declare function createProgram(version?: string): Command;
@@ -0,0 +1,15 @@
1
+ export interface RenderOptions {
2
+ file?: string;
3
+ theme?: string;
4
+ customTheme?: string;
5
+ highlight: string;
6
+ macStyle: boolean;
7
+ footnote: boolean;
8
+ }
9
+ export interface ThemeOptions {
10
+ list?: boolean;
11
+ add?: boolean;
12
+ name?: string;
13
+ path?: string;
14
+ rm?: string;
15
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenyan-md/cli",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "A CLI tool for Wenyan markdown rendering & publishing",
5
5
  "author": "Lei <caol64@gmail.com> (https://github.com/caol64)",
6
6
  "license": "Apache-2.0",
@@ -15,13 +15,12 @@
15
15
  ],
16
16
  "type": "module",
17
17
  "bin": {
18
- "wenyan": "./bin/cli.js"
18
+ "wenyan": "./dist/cli.js"
19
19
  },
20
20
  "main": "./dist/index.js",
21
21
  "types": "./dist/types/index.d.ts",
22
22
  "files": [
23
- "dist",
24
- "bin"
23
+ "dist"
25
24
  ],
26
25
  "homepage": "https://github.com/caol64/wenyan-cli#readme",
27
26
  "repository": {
@@ -32,20 +31,27 @@
32
31
  "url": "https://github.com/caol64/wenyan-cli/issues"
33
32
  },
34
33
  "dependencies": {
35
- "@wenyan-md/core": "^1.0.17",
36
- "commander": "^14.0.0"
34
+ "@wenyan-md/core": "^2.0.2",
35
+ "commander": "^14.0.0",
36
+ "form-data-encoder": "^4.1.0",
37
+ "formdata-node": "^6.0.3",
38
+ "jsdom": "^27.4.0"
37
39
  },
38
40
  "devDependencies": {
39
41
  "@types/node": "^24.3.0",
40
42
  "dotenv-cli": "^10.0.0",
41
- "tsx": "^4.20.5",
42
- "typescript": "^5.9.2"
43
+ "tsx": "^4.21.0",
44
+ "typescript": "^5.9.2",
45
+ "vite": "^7.3.1",
46
+ "vitest": "^4.0.18"
43
47
  },
44
48
  "scripts": {
45
49
  "build": "tsc",
46
- "dev": "tsx src/index.ts",
47
50
  "upgrade:core": "pnpm update @wenyan-md/core",
48
- "test:render": "pnpm build && node ./bin/cli.js render -f test/publish.md",
49
- "test:publish": "pnpm build && pnpm dotenv -e .env.test -- node ./bin/cli.js publish -f test/publish.md -t phycat"
51
+ "test:bin": "pnpm build && node ./dist/cli.js render -f tests/publish.md -c tests/manhua.css --no-mac-style",
52
+ "test:cli": "vitest run tests/cli.test.ts",
53
+ "test:render": "vitest run tests/render.test.ts",
54
+ "test:publish": "vitest run tests/publish.test.ts",
55
+ "test:realPublish": "pnpm build && pnpm dotenv -e .env.test -- node ./dist/cli.js publish -f tests/publish.md -c tests/manhua.css --no-mac-style"
50
56
  }
51
57
  }
package/bin/cli.js DELETED
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { fileURLToPath, pathToFileURL } from 'url';
4
- import { dirname, join } from 'path';
5
-
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const mainModulePath = join(__dirname, "..", "dist", 'index.js');
8
-
9
- import(pathToFileURL(mainModulePath).href);