grande-iconfont-sync 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bin/cli.cjs ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Iconfont Sync CLI
5
+ * 命令行直接同步入口
6
+ */
7
+
8
+ const startSync = require('../src/server.cjs');
9
+
10
+ startSync()
11
+ .then((result) => {
12
+ if (result.isError) {
13
+ process.exit(1);
14
+ }
15
+ process.exit(0);
16
+ })
17
+ .catch((error) => {
18
+ console.error('❌ 执行失败:', error.message);
19
+ process.exit(1);
20
+ });
package/bin/mcp.cjs ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Iconfont Sync MCP Server
5
+ * MCP 服务器入口,用于 AI 工具集成
6
+ */
7
+
8
+ require('../index.cjs');
package/index.cjs ADDED
@@ -0,0 +1,60 @@
1
+ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
2
+ const {
3
+ StdioServerTransport,
4
+ } = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ const { z } = require("zod");
6
+ const startSync = require("./src/server.cjs");
7
+
8
+
9
+ // ============ 创建 MCP Server ============
10
+ function createServer(apiKeyGetter) {
11
+ const server = new McpServer({
12
+ name: "iconfont-sync-mcp-server",
13
+ version: "1.0.0",
14
+ });
15
+
16
+ // 工具: 自动同步阿里 iconfont 项目文件
17
+ server.tool(
18
+ "iconfont-sync",
19
+ "CLI tool for automatically synchronizing Ali iconfont project files to the local area, which supports comparing file differences (ignoring timestamps) and automatically updating new/changed files. / 自动同步阿里 iconfont 项目文件到本地的 CLI 工具,支持对比文件差异(忽略时间戳)、自动更新新增/变更文件",
20
+ {},
21
+ async () => {
22
+ try {
23
+ console.error("[iconfont-sync] Starting...");
24
+ const syncedInfo = await startSync();
25
+ console.error("[iconfont-sync] Synced:", syncedInfo);
26
+ return {
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text: JSON.stringify(syncedInfo, null, 2),
31
+ },
32
+ ],
33
+ };
34
+ } catch (error) {
35
+ console.error(`[Error] iconfont-sync:`, error.message);
36
+ return {
37
+ isError: true,
38
+ content: [{ type: "text", text: `Query failed: ${error.message}` }],
39
+ };
40
+ }
41
+ }
42
+ );
43
+
44
+ return server;
45
+ }
46
+
47
+ // ============ 主启动逻辑 ============
48
+ async function main() {
49
+ // STDIO 模式
50
+ console.error("[STDIO] Starting...");
51
+ const server = createServer();
52
+ const transport = new StdioServerTransport();
53
+ await server.connect(transport);
54
+ console.error("[STDIO] Connected");
55
+ }
56
+
57
+ main().catch((error) => {
58
+ console.error("[Fatal]", error);
59
+ process.exit(1);
60
+ });
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "grande-iconfont-sync",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool and MCP server for automatically synchronizing Alibaba iconfont project files to local, supporting file difference comparison (ignoring timestamps) and automatic updates.",
5
+ "type": "module",
6
+ "main": "index.cjs",
7
+ "bin": {
8
+ "iconfont-sync-mcp": "./bin/mcp.cjs",
9
+ "iconfont-sync": "./bin/cli.cjs"
10
+ },
11
+ "scripts": {
12
+ "start": "node index.cjs",
13
+ "sync": "node bin/cli.cjs"
14
+ },
15
+ "keywords": [
16
+ "iconfont",
17
+ "sync",
18
+ "cli",
19
+ "阿里图标库",
20
+ "自动同步",
21
+ "iconfont同步",
22
+ "alibaba",
23
+ "icon",
24
+ "font",
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "automation"
28
+ ],
29
+ "author": "Grande",
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=16.0.0"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/123liu950/iconfont-sync"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/123liu950/iconfont-sync/issues"
40
+ },
41
+ "homepage": "https://github.com/123liu950/iconfont-sync#readme",
42
+ "files": [
43
+ "bin/",
44
+ "src/",
45
+ "index.cjs",
46
+ "README.md",
47
+ "LICENSE"
48
+ ],
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.25.3",
51
+ "adm-zip": "^0.5.16",
52
+ "axios": "^1.13.4",
53
+ "clsx": "2.1.1",
54
+ "jsencrypt": "^3.5.4"
55
+ },
56
+ "devDependencies": {
57
+ "@tailwindcss/vite": "4.1.17",
58
+ "@types/node": "^22.0.0"
59
+ }
60
+ }
package/src/server.cjs ADDED
@@ -0,0 +1,618 @@
1
+ /**
2
+ * Iconfont 同步脚本
3
+ * 功能:比较远端 iconfont 与本地文件,如有更新则自动替换
4
+ *
5
+ * 使用方法:
6
+ * node scripts/iconfont-sync.cjs --account=你的账号 --password=你的密码 --pid=项目ID --localPath=本地路径 --files=file1.css,file2.js
7
+ */
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const crypto = require("crypto");
12
+ const AdmZip = require("adm-zip");
13
+ const JSEncrypt = require("jsencrypt");
14
+ const axios = require("axios");
15
+ const os = require("os");
16
+
17
+ // ============ 配置区域 ============
18
+ const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
19
+ MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGa4CR/fcRUFv2r+YdiRXBDGqi4E
20
+ 0HO1Eu0FqvVJtlvXrrxXGHzul+iFR8zO1xKapNhW60pkEpB/jbXUhog7q0R54cSL
21
+ bS+4SRv80M2YAdTkaO+frP2j1LyGtNquw/W7oj0+SskEL+U6Yn1a27uHhGbl4BBf
22
+ TM9FXzxEEomKPoMRAgMBAAE=
23
+ -----END PUBLIC KEY-----`;
24
+
25
+ // 默认需要同步的文件列表
26
+ const DEFAULT_FILES = [
27
+ "iconfont.css",
28
+ "iconfont.js",
29
+ "iconfont.json",
30
+ "iconfont.ttf",
31
+ "iconfont.woff",
32
+ "iconfont.woff2",
33
+ "iconfont.eot",
34
+ "iconfont.svg",
35
+ ];
36
+
37
+ // ============ 全局变量 ============
38
+ const globalCookie = {};
39
+
40
+ // ============ 解析命令行参数 ============
41
+ // function parseArgs() {
42
+ // // 优先取配置环境变量
43
+ // const envArgs = process.env || {};
44
+ // if (
45
+ // Object.values(envArgs).length > 0 &&
46
+ // envArgs.account &&
47
+ // envArgs.password &&
48
+ // envArgs.pid &&
49
+ // envArgs.localPath
50
+ // ) {
51
+ // return envArgs;
52
+ // }
53
+
54
+ // // 读取命令行参数
55
+ // const args = process.argv.slice(2);
56
+ // const config = {
57
+ // account: "",
58
+ // password: "",
59
+ // pid: "",
60
+ // localPath: "",
61
+ // files: [],
62
+ // };
63
+
64
+ // for (const arg of args) {
65
+ // if (arg.startsWith("--")) {
66
+ // const [key, value] = arg.slice(2).split("=");
67
+ // if (key === "files") {
68
+ // config.files = value ? value.split(",").map((f) => f.trim()) : [];
69
+ // } else {
70
+ // config[key] = value || "";
71
+ // }
72
+ // }
73
+ // }
74
+
75
+ // return config;
76
+ // }
77
+ // ============ 解析命令行参数 ============
78
+ function parseArgs() {
79
+ const env = process.env || {};
80
+
81
+ // 检查是否有相关环境变量配置
82
+ if (env.account || env.password || env.pid || env.localPath) {
83
+ const config = {
84
+ account: env.account || '',
85
+ password: env.password || '',
86
+ pid: env.pid || '',
87
+ localPath: env.localPath || '',
88
+ files: []
89
+ };
90
+
91
+ // 关键修改:将逗号分隔的字符串转为数组
92
+ if (env.files && typeof env.files === 'string') {
93
+ config.files = env.files.split(',').map(f => f.trim()).filter(f => f);
94
+ } else if (Array.isArray(env.files)) {
95
+ config.files = env.files;
96
+ }
97
+
98
+ return config;
99
+ }
100
+
101
+ // 读取命令行参数
102
+ const args = process.argv.slice(2);
103
+ const config = {
104
+ account: '',
105
+ password: '',
106
+ pid: '',
107
+ localPath: '',
108
+ files: []
109
+ };
110
+
111
+ for (const arg of args) {
112
+ if (arg.startsWith('--')) {
113
+ const [key, value] = arg.slice(2).split('=');
114
+ if (key === 'files') {
115
+ config.files = value ? value.split(',').map(f => f.trim()) : [];
116
+ } else {
117
+ config[key] = value || '';
118
+ }
119
+ }
120
+ }
121
+
122
+ return config;
123
+ }
124
+
125
+ // ============ 验证配置 ============
126
+ function validateConfig(config) {
127
+ const errors = [];
128
+
129
+ if (!config.account) {
130
+ errors.push("缺少 --account 参数(iconfont 账号)");
131
+ }
132
+ if (!config.password) {
133
+ errors.push("缺少 --password 参数(iconfont 密码)");
134
+ }
135
+ if (!config.pid) {
136
+ errors.push("缺少 --pid 参数(iconfont 项目 ID)");
137
+ }
138
+ if (!config.localPath) {
139
+ errors.push("缺少 --localPath 参数(本地 iconfont 文件夹路径)");
140
+ }
141
+
142
+ if (errors.length > 0) {
143
+ console.error("❌ 配置错误:");
144
+ errors.forEach((err) => console.error(` - ${err}`));
145
+ console.log("\n📖 使用方法:");
146
+ console.log(" node scripts/iconfont-sync.cjs \\");
147
+ console.log(" --account=your_account \\");
148
+ console.log(" --password=your_password \\");
149
+ console.log(" --pid=your_project_id \\");
150
+ console.log(" --localPath=./src/icons \\");
151
+ console.log(" --files=iconfont.css,iconfont.js");
152
+ process.exit(1);
153
+ }
154
+
155
+ // 如果没有指定 files,使用默认列表
156
+ if (config.files.length === 0) {
157
+ config.files = DEFAULT_FILES;
158
+ }
159
+
160
+ return config;
161
+ }
162
+
163
+ // ============ Cookie 处理 ============
164
+ function getCookieString() {
165
+ return Object.entries(globalCookie)
166
+ .filter(([_, value]) => value !== undefined)
167
+ .map(([key, value]) => `${key}=${value}`)
168
+ .join("; ");
169
+ }
170
+
171
+ function updateGlobalCookie(headers) {
172
+ const cookies = headers["set-cookie"];
173
+ if (!cookies) return;
174
+
175
+ for (const cookie of cookies) {
176
+ const [mainPart] = cookie.split("; ");
177
+ if (!mainPart) continue;
178
+ const [key, value] = mainPart.split("=");
179
+ if (key) globalCookie[key] = value || "";
180
+ }
181
+ }
182
+
183
+ // ============ 登录 ============
184
+ async function login(config) {
185
+ console.log("🔑 正在登录 iconfont...");
186
+
187
+ try {
188
+ const encrypt = new JSEncrypt();
189
+ encrypt.setPublicKey(PUBLIC_KEY);
190
+ const encryptedPwd = encrypt.encrypt(config.password);
191
+
192
+ if (!encryptedPwd) {
193
+ throw new Error("密码加密失败");
194
+ }
195
+
196
+ const resp = await axios.post(
197
+ "https://www.iconfont.cn/api/account/login.json",
198
+ {
199
+ target: config.account,
200
+ password: encryptedPwd,
201
+ },
202
+ {
203
+ headers: {
204
+ "Content-Type": "application/json",
205
+ "User-Agent": "Mozilla/5.0 (Node.js) axios/1.6.0",
206
+ },
207
+ },
208
+ );
209
+
210
+ if (resp.data.code !== 200) {
211
+ throw new Error(`登录失败: ${resp.data.message || "未知错误"}`);
212
+ }
213
+
214
+ updateGlobalCookie(resp.headers);
215
+ console.log("✅ 登录成功");
216
+ return true;
217
+ } catch (error) {
218
+ console.error("❌ 登录失败:", error.message);
219
+ return false;
220
+ }
221
+ }
222
+
223
+ // ============ 下载图标包 ============
224
+ async function downloadIcons(config, tempDir) {
225
+ console.log("📥 正在下载图标包...");
226
+
227
+ try {
228
+ const resp = await axios.get(
229
+ "https://www.iconfont.cn/api/project/download.zip",
230
+ {
231
+ params: { pid: config.pid },
232
+ headers: {
233
+ Cookie: getCookieString(),
234
+ Accept:
235
+ "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
236
+ "User-Agent": "Mozilla/5.0 (Node.js) axios/1.6.0",
237
+ },
238
+ responseType: "arraybuffer",
239
+ },
240
+ );
241
+
242
+ updateGlobalCookie(resp.headers);
243
+
244
+ const zipPath = path.join(tempDir, "iconfont.zip");
245
+ fs.writeFileSync(zipPath, resp.data);
246
+
247
+ console.log("✅ 下载完成");
248
+ return zipPath;
249
+ } catch (error) {
250
+ console.error("❌ 下载失败:", error.message);
251
+ throw error;
252
+ }
253
+ }
254
+
255
+ // ============ 解压图标包 ============
256
+ function unzipIcons(zipPath, tempDir) {
257
+ console.log("📦 正在解压图标包...");
258
+
259
+ try {
260
+ const zip = new AdmZip(zipPath);
261
+ zip.extractAllTo(tempDir, true);
262
+
263
+ // 将 font_xxx 文件夹中的文件移动到临时目录根目录
264
+ const items = fs.readdirSync(tempDir);
265
+ for (const item of items) {
266
+ const itemPath = path.join(tempDir, item);
267
+ if (item.startsWith("font_") && fs.statSync(itemPath).isDirectory()) {
268
+ const files = fs.readdirSync(itemPath);
269
+ for (const file of files) {
270
+ const oldPath = path.join(itemPath, file);
271
+ const newPath = path.join(tempDir, file);
272
+ // 跳过目录,只处理文件
273
+ if (fs.statSync(oldPath).isDirectory()) {
274
+ continue;
275
+ }
276
+ if (fs.existsSync(newPath)) {
277
+ fs.unlinkSync(newPath);
278
+ }
279
+ fs.renameSync(oldPath, newPath);
280
+ }
281
+ fs.rmSync(itemPath, { recursive: true, force: true });
282
+ }
283
+ }
284
+
285
+ // 删除 zip 文件
286
+ if (fs.existsSync(zipPath)) {
287
+ fs.unlinkSync(zipPath);
288
+ }
289
+
290
+ console.log("✅ 解压完成");
291
+ return true;
292
+ } catch (error) {
293
+ console.error("❌ 解压失败:", error.message);
294
+ throw error;
295
+ }
296
+ }
297
+
298
+ // ============ 规范化文件内容 ============
299
+ /**
300
+ * 对文件内容进行规范化处理,去除动态内容(如时间戳)
301
+ * 这样可以避免因时间戳不同而导致的误判
302
+ */
303
+ function normalizeContent(content, fileName) {
304
+ // 只对文本文件进行规范化
305
+ const textExtensions = [".css", ".js", ".json", ".html", ".svg"];
306
+ const ext = path.extname(fileName).toLowerCase();
307
+
308
+ if (!textExtensions.includes(ext)) {
309
+ // 二进制文件(如 woff, woff2, ttf, eot)不进行规范化
310
+ return content;
311
+ }
312
+
313
+ let text = content.toString("utf-8");
314
+
315
+ // 1. 去除 CSS 中的时间戳参数,如 ?t=1702345678
316
+ text = text.replace(/\?t=\d+/g, "");
317
+
318
+ // 2. 去除 JS 中的时间戳,如 "t": "1702345678" 或 't': '1702345678'
319
+ text = text.replace(/"t"\s*:\s*"\d+"/g, '"t": ""');
320
+ text = text.replace(/'t'\s*:\s*'\d+'/g, "'t': ''");
321
+
322
+ // 3. 去除可能的版本号时间戳,如 v=1702345678
323
+ text = text.replace(/[?&]v=\d+/g, "");
324
+
325
+ // 4. 去除 iconfont 生成的日期注释,如 /* Created on 2024-01-01 */
326
+ text = text.replace(/\/\*\s*Created on \d{4}-\d{2}-\d{2}.*?\*\//g, "");
327
+
328
+ // 5. 去除 font_xxxx 格式的动态文件夹名引用
329
+ text = text.replace(/font_\d+_\w+/g, "font_normalized");
330
+
331
+ return Buffer.from(text, "utf-8");
332
+ }
333
+
334
+ // ============ 检查是否为文件 ============
335
+ function isFile(filePath) {
336
+ try {
337
+ if (!fs.existsSync(filePath)) {
338
+ return false;
339
+ }
340
+ const stat = fs.statSync(filePath);
341
+ return stat.isFile();
342
+ } catch (error) {
343
+ return false;
344
+ }
345
+ }
346
+
347
+ // ============ 计算文件 MD5(规范化后)============
348
+ function getFileMD5(filePath, fileName, normalize = true) {
349
+ if (!fs.existsSync(filePath)) {
350
+ return null;
351
+ }
352
+
353
+ // 检查是否是目录
354
+ try {
355
+ const stat = fs.statSync(filePath);
356
+ if (stat.isDirectory()) {
357
+ return null;
358
+ }
359
+ } catch (error) {
360
+ return null;
361
+ }
362
+
363
+ let content = fs.readFileSync(filePath);
364
+
365
+ if (normalize && fileName) {
366
+ content = normalizeContent(content, fileName);
367
+ }
368
+
369
+ return crypto.createHash("md5").update(content).digest("hex");
370
+ }
371
+
372
+ // ============ 判断是否为二进制文件 ============
373
+ function isBinaryFile(fileName) {
374
+ const binaryExtensions = [".woff", ".woff2", ".ttf", ".eot", ".otf"];
375
+ const ext = path.extname(fileName).toLowerCase();
376
+ return binaryExtensions.includes(ext);
377
+ }
378
+
379
+ // ============ 获取关联的 CSS 文件变化状态 ============
380
+ function shouldUpdateBinaryBasedOnCSS(fileName, config, tempDir) {
381
+ // 如果是字体文件,检查对应的 CSS 文件是否有变化
382
+ // 如果 CSS 有变化,说明图标有实际更新,字体文件也需要更新
383
+ if (!isBinaryFile(fileName)) {
384
+ return null; // 非二进制文件,返回 null 表示需要正常比较
385
+ }
386
+
387
+ const cssFileName = "iconfont.css";
388
+ const remoteCssPath = path.join(tempDir, cssFileName);
389
+ const localCssPath = path.join(config.localPath, cssFileName);
390
+
391
+ if (!isFile(remoteCssPath)) {
392
+ return null; // CSS 不存在或不是文件,正常比较
393
+ }
394
+
395
+ const remoteCssMD5 = getFileMD5(remoteCssPath, cssFileName, true);
396
+ const localCssMD5 = getFileMD5(localCssPath, cssFileName, true);
397
+
398
+ // 如果 CSS 内容(去除时间戳后)相同,则字体文件也视为相同
399
+ if (remoteCssMD5 === localCssMD5) {
400
+ return false; // 不需要更新
401
+ }
402
+
403
+ return true; // CSS 有变化,需要更新字体
404
+ }
405
+
406
+ // ============ 比较并更新文件 ============
407
+ function compareAndUpdate(config, tempDir) {
408
+ console.log("\n🔍 开始比较文件差异...");
409
+ console.log(` 本地路径: ${config.localPath}`);
410
+ console.log(` 对比文件: ${config.files}`);
411
+ console.log(
412
+ " 📝 注意: 文本文件会忽略时间戳差异,字体文件根据 CSS 变化判断",
413
+ );
414
+ console.log("");
415
+
416
+ const results = {
417
+ updated: [],
418
+ added: [],
419
+ unchanged: [],
420
+ notFound: [],
421
+ };
422
+
423
+ // 确保本地目录存在
424
+ if (!fs.existsSync(config.localPath)) {
425
+ fs.mkdirSync(config.localPath, { recursive: true });
426
+ console.log(`📁 创建本地目录: ${config.localPath}`);
427
+ }
428
+
429
+ // 遍历需要检查的文件
430
+ for (const fileName of config.files) {
431
+ const remotePath = path.join(tempDir, fileName);
432
+ const localPath = path.join(config.localPath, fileName);
433
+
434
+ // 检查远端文件是否存在且是文件(不是目录)
435
+ if (!isFile(remotePath)) {
436
+ results.notFound.push(fileName);
437
+ console.log(` ⚠️ ${fileName} - 远端不存在或不是文件`);
438
+ continue;
439
+ }
440
+
441
+ // 本地文件不存在,需要新增
442
+ if (!fs.existsSync(localPath)) {
443
+ fs.copyFileSync(remotePath, localPath);
444
+ results.added.push(fileName);
445
+ console.log(` ➕ ${fileName} - 新增文件`);
446
+ continue;
447
+ }
448
+
449
+ // 本地路径存在但是目录,跳过
450
+ if (!isFile(localPath)) {
451
+ results.notFound.push(fileName);
452
+ console.log(` ⚠️ ${fileName} - 本地路径是目录而非文件`);
453
+ continue;
454
+ }
455
+
456
+ // 对于二进制字体文件,根据 CSS 文件的变化来判断是否需要更新
457
+ if (isBinaryFile(fileName)) {
458
+ const shouldUpdate = shouldUpdateBinaryBasedOnCSS(
459
+ fileName,
460
+ config,
461
+ tempDir,
462
+ );
463
+
464
+ if (shouldUpdate === false) {
465
+ // CSS 无变化,字体文件也视为无变化
466
+ results.unchanged.push(fileName);
467
+ console.log(` ✓ ${fileName} - 无变化 (基于 CSS 判断)`);
468
+ continue;
469
+ } else if (shouldUpdate === true) {
470
+ // CSS 有变化,更新字体文件
471
+ fs.copyFileSync(remotePath, localPath);
472
+ results.updated.push(fileName);
473
+ console.log(` 🔄 ${fileName} - 已更新 (CSS 已变化)`);
474
+ continue;
475
+ }
476
+ // shouldUpdate === null,走正常比较流程
477
+ }
478
+
479
+ // 使用规范化后的内容进行 MD5 比较(去除时间戳等动态内容)
480
+ const remoteMD5 = getFileMD5(remotePath, fileName, true);
481
+ const localMD5 = getFileMD5(localPath, fileName, true);
482
+
483
+ if (remoteMD5 !== localMD5) {
484
+ // 文件内容不同,需要更新
485
+ fs.copyFileSync(remotePath, localPath);
486
+ results.updated.push(fileName);
487
+ console.log(` 🔄 ${fileName} - 已更新`);
488
+ } else {
489
+ // 文件相同,无需更新
490
+ results.unchanged.push(fileName);
491
+ console.log(` ✓ ${fileName} - 无变化`);
492
+ }
493
+ }
494
+
495
+ return results;
496
+ }
497
+
498
+ // ============ 清理临时目录 ============
499
+ function cleanupTempDir(tempDir) {
500
+ try {
501
+ fs.rmSync(tempDir, { recursive: true, force: true });
502
+ } catch (error) {
503
+ console.warn("⚠️ 清理临时目录失败:", error.message);
504
+ }
505
+ }
506
+
507
+ // ============ 打印统计结果 ============
508
+ function printSummary(results) {
509
+ console.log("\n" + "═".repeat(50));
510
+ console.log("📊 同步统计");
511
+ console.log("═".repeat(50));
512
+
513
+ let syncedInfo = {};
514
+
515
+ if (results.added.length > 0) {
516
+ syncedInfo.addedText = `新增: ${results.added.length} 个文件`;
517
+ console.log(` ➕ 新增: ${results.added.length} 个文件`);
518
+ results.added.forEach((f) => {
519
+ console.log(` - ${f}`);
520
+ syncedInfo.addedText += `\n - ${f}`;
521
+ });
522
+ }
523
+
524
+ if (results.updated.length > 0) {
525
+ syncedInfo.updatedText = `更新: ${results.updated.length} 个文件`;
526
+ console.log(` 🔄 更新: ${results.updated.length} 个文件`);
527
+ results.updated.forEach((f) => {
528
+ console.log(` - ${f}`);
529
+ syncedInfo.updatedText += `\n - ${f}`;
530
+ });
531
+ }
532
+
533
+ if (results.unchanged.length > 0) {
534
+ syncedInfo.unchangedText = `无变化: ${results.unchanged.length} 个文件`;
535
+ console.log(` ✓ 无变化: ${results.unchanged.length} 个文件`);
536
+ }
537
+
538
+ if (results.notFound.length > 0) {
539
+ syncedInfo.notFoundText = `远端不存在: ${results.notFound.length} 个文件`;
540
+ console.log(` ⚠️ 远端不存在: ${results.notFound.length} 个文件`);
541
+ results.notFound.forEach((f) => {
542
+ console.log(` - ${f}`);
543
+ syncedInfo.notFoundText += `\n - ${f}`;
544
+ });
545
+ }
546
+
547
+ console.log("═".repeat(50));
548
+
549
+ const hasChanges = results.added.length > 0 || results.updated.length > 0;
550
+ if (hasChanges) {
551
+ console.log("🎉 同步完成!有文件被更新。");
552
+ } else {
553
+ console.log("ℹ️ 同步完成!所有文件已是最新。");
554
+ }
555
+
556
+ return syncedInfo;
557
+ }
558
+
559
+ // ============ 主函数 ============
560
+ async function startSync() {
561
+ console.log("");
562
+ console.log("╔════════════════════════════════════════════════╗");
563
+ console.log("║ Iconfont 同步工具 v1.0.0 ║");
564
+ console.log("╚════════════════════════════════════════════════╝");
565
+ console.log("");
566
+
567
+ let syncedInfo = {};
568
+
569
+ // 解析并验证配置
570
+ const config = validateConfig(parseArgs());
571
+
572
+ // 转换为绝对路径
573
+ config.localPath = path.resolve(process.cwd(), config.localPath);
574
+
575
+ // 创建临时目录
576
+ const tempDir = path.join(os.tmpdir(), `iconfont-sync-${Date.now()}`);
577
+ fs.mkdirSync(tempDir, { recursive: true });
578
+
579
+ try {
580
+ // 1. 登录
581
+ const loginSuccess = await login(config);
582
+ if (!loginSuccess) {
583
+ throw new Error("登录失败,请检查账号密码");
584
+ }
585
+
586
+ // 2. 下载图标包
587
+ const zipPath = await downloadIcons(config, tempDir);
588
+
589
+ // 3. 解压
590
+ unzipIcons(zipPath, tempDir);
591
+
592
+ // 4. 比较并更新文件
593
+ const results = compareAndUpdate(config, tempDir);
594
+
595
+ // 5. 打印统计结果
596
+ syncedInfo = printSummary(results);
597
+
598
+ // 6. 返回退出码
599
+ const hasErrors = results.notFound.length === config.files.length;
600
+ // process.exit(hasErrors ? 1 : 0);
601
+ } catch (error) {
602
+ console.error("\n❌ 同步失败:", error.message);
603
+ syncedInfo = {
604
+ isError: true,
605
+ error: error.message,
606
+ };
607
+ } finally {
608
+ // 清理临时目录
609
+ cleanupTempDir(tempDir);
610
+
611
+ return syncedInfo;
612
+ }
613
+ }
614
+
615
+ // 启动
616
+ // startSync();
617
+
618
+ module.exports = startSync;