@zzclub/z-cli 0.4.5

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,57 @@
1
+ import { writeFileContent, getLocalConfig } from "../utils/common.js";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import path from "node:path";
5
+ import { dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import os from 'node:os'
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ export const setCmd = {
13
+ name: "set <configName> [payload...]",
14
+ description:
15
+ "设置全局config.json配置, 只有两个命令, 复制即可 \n 1. zz set translate account.appId <value> \n 2. zz set translate account.key <value>",
16
+ options: [],
17
+ action: async (configName, payload, cmd) => {
18
+ let set_spinner = ora();
19
+ let config = await getLocalConfig();
20
+ let configItem = config[configName];
21
+ if (configItem) {
22
+ if (config.editableConfig.includes(configName)) {
23
+ if (payload.length === 2) {
24
+ let key = payload[0];
25
+ if (!["account.key", "account.appId"].includes(key)) {
26
+ set_spinner.warn(`设置appId: zz set translate appId <value>`);
27
+ set_spinner.warn(`设置key: zz set translate key <value>`);
28
+ process.exit(1);
29
+ }
30
+ let value = payload[1];
31
+ eval(`configItem.${key} = "${value}"`);
32
+ // configItem[key] = value
33
+ let configContent = JSON.stringify(config, null, 2);
34
+ writeFileContent(
35
+ path.resolve(__dirname, "../config.json"),
36
+ configContent,
37
+ (spinner, isOk) => {
38
+ if (isOk) {
39
+ } else {
40
+ spinner.fail("配置失败");
41
+ }
42
+ }
43
+ );
44
+ } else {
45
+ set_spinner.warn(`设置appId: zz set translate account.appId <value>`);
46
+ set_spinner.warn(`设置key: zz set translate account.key <value>`);
47
+ process.exit(1);
48
+ }
49
+ } else {
50
+ set_spinner.fail(`配置项[${chalk.red(configName)}]不允许修改`);
51
+ }
52
+ } else {
53
+ set_spinner.fail(`配置项[${chalk.red(configName)}]不存在`);
54
+ process.exit(1);
55
+ }
56
+ },
57
+ };
@@ -0,0 +1,328 @@
1
+ import {
2
+ writeFileContent,
3
+ getLocalConfig,
4
+ setHighLightStr,
5
+ } from "../utils/common.js";
6
+
7
+ import { getFormatedFileSize, replaceFileContent } from "../utils/file.js";
8
+ import chalk from "chalk";
9
+ import ora from "ora";
10
+ import path from "node:path";
11
+ import { dirname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import fs from "node:fs";
14
+ import sharp from "sharp";
15
+ import { picgoCmd } from "./picgo.js";
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+
20
+ const supportFileTypes = ["png", "jpg", "jpeg", "gif", "webp"];
21
+
22
+ export const tinyCmd = {
23
+ name: "tiny",
24
+ description: "压缩图片",
25
+ options: [
26
+ {
27
+ flags: "-t, --type <fileType>",
28
+ description: "转换后的图片类型",
29
+ defaultValue: null,
30
+ },
31
+ {
32
+ flags: "-f, --file <file>",
33
+ description: "要压缩的图片文件",
34
+ defaultValue: null,
35
+ },
36
+ {
37
+ flags: "-d, --dir <dir>",
38
+ description: "压缩文件夹内所有文件",
39
+ defaultValue: null,
40
+ },
41
+ {
42
+ flags: "-co, --condition <condition>",
43
+ description: "压缩文件夹内所有名称包含[--condition]的图片文件",
44
+ defaultValue: null,
45
+ },
46
+ {
47
+ flags: "-q, --quality <quality>",
48
+ description: "压缩质量(1-100)",
49
+ defaultValue: 75,
50
+ },
51
+ {
52
+ flags: "-c, --colours <colours>",
53
+ description: "GIF色彩保留(2-256)",
54
+ defaultValue: 128,
55
+ },
56
+ {
57
+ flags: "-n, --name <name>",
58
+ description: "指定文件名输出",
59
+ defaultValue: "",
60
+ },
61
+ {
62
+ flags: "-m, --max <max>",
63
+ description: "限制要上传的文件大小(仅当开启 --picgo 时会用到)",
64
+ defaultValue: 60,
65
+ },
66
+ {
67
+ flags: "--picgo [type]",
68
+ description: "是否调用picgo",
69
+ defaultValue: null,
70
+ },
71
+ {
72
+ flags: "--no-picgo [type]",
73
+ description: "是否调用picgo",
74
+ defaultValue: null,
75
+ },
76
+ {
77
+ flags: "-ref, --replace-file <replaceFile>",
78
+ description: "要替换内容的文件",
79
+ defaultValue: "",
80
+ },
81
+ {
82
+ flags: "--no-replace [type]",
83
+ description:
84
+ "是否替换指定文件的内容, 替换规则斤仅针对Obsidian, 请看readme.md",
85
+ defaultValue: null,
86
+ },
87
+ {
88
+ flags: "--replace [type]",
89
+ description:
90
+ "是否替换指定文件的内容, 替换规则斤仅针对Obsidian, 请看readme.md",
91
+ defaultValue: null,
92
+ },
93
+ ],
94
+ action: async (option) => {
95
+ let spinner = ora();
96
+ let config = await getLocalConfig();
97
+ // console.log(`OPTION`, option);
98
+ let { type, file, dir, condition, quality, colours } = option;
99
+
100
+ if (!file && !dir && !condition) {
101
+ spinner.fail("请指定要压缩的文件(--file=xxx)或文件夹(--dir=/a/b)");
102
+ process.exit(1);
103
+ }
104
+
105
+ // 是否需要替换内容, true时 需要收集list<Map>
106
+ let isNeedUploadAndReplace = !!(
107
+ // option.picgo &&
108
+ (option.replace && option.replaceFile)
109
+ );
110
+ let replaceMaps = [];
111
+
112
+ function collectReplaceMaps(replaceMaps, { text, newText }) {
113
+ if (isNeedUploadAndReplace) {
114
+ let map = {
115
+ text, // obsidian md 文件中 粘贴后的 值
116
+ newText, // 此时是压缩后的文件名, 在picgo里再替换成上传后的
117
+ };
118
+ replaceMaps.push(map);
119
+ }
120
+ }
121
+ async function tinyFile(file, statisticsCount, options = {}) {
122
+ try {
123
+ let stats = fs.statSync(file);
124
+ let isFile = stats.isFile();
125
+ if (isFile) {
126
+ // 文件类型
127
+ let extname = path.extname(file);
128
+ // 带后缀的文件名
129
+ let fileName = path.basename(file)?.split(".")[0];
130
+ // 文件路径
131
+ let dirPath = path.dirname(file);
132
+ // 文件类型
133
+ let filetype = extname.slice(1);
134
+ let fileSize = stats.size;
135
+ let beforeSize = getFormatedFileSize(fileSize);
136
+ // 是否在支持的格式里
137
+ // 如果正在批量压缩, 并且要求指定名称输出, 则加一个index后缀, 避免重名
138
+ let customFileName;
139
+ if (options.count && options.count > 1 && option.name) {
140
+ customFileName = `${option.name}${options.order + 1}`;
141
+ } else {
142
+ customFileName = option.name || `${fileName}-tiny`;
143
+ }
144
+
145
+ let outputPath =
146
+ path.resolve(process.cwd(), dirPath) +
147
+ "/" +
148
+ customFileName +
149
+ extname;
150
+ let inputPath = path.resolve(process.cwd(), file);
151
+ if (extname.slice(1) === "gif") {
152
+ // console.log(`option.colours`, option.colours);
153
+ await sharp(inputPath, {
154
+ animated: true,
155
+ limitInputPixels: false,
156
+ })
157
+ .gif({
158
+ colours: +option.colours,
159
+ })
160
+ .toFile(outputPath);
161
+ let afterStats = fs.statSync(outputPath);
162
+ let afterSize = getFormatedFileSize(afterStats.size);
163
+ let offPercent = ((1 - afterStats.size / fileSize) * 100).toFixed(
164
+ 2
165
+ );
166
+ spinner.succeed(
167
+ `${chalk.red(beforeSize)} ${chalk.yellowBright(
168
+ "=>"
169
+ )} ${chalk.green(afterSize)} (${
170
+ offPercent >= 0 ? chalk.green("↓") : chalk.red("↑")
171
+ }${Math.abs(offPercent)}%)【 ${customFileName}${extname} 】`
172
+ );
173
+ // 收集压缩前后的文件名映射关系
174
+ collectReplaceMaps(replaceMaps, {
175
+ text: `${path.basename(file)}`,
176
+ newText: `${customFileName}${extname}`,
177
+ });
178
+ statisticsCount.ok++;
179
+ statisticsCount.okFiles.push(outputPath);
180
+ } else {
181
+ if (sharp(inputPath)[filetype]) {
182
+ await sharp(path.resolve(process.cwd(), file))
183
+ [filetype]({ quality: +quality })
184
+ .toFile(outputPath);
185
+ let afterStats = fs.statSync(outputPath);
186
+ let afterSize = getFormatedFileSize(afterStats.size);
187
+ let offPercent = ((1 - afterStats.size / fileSize) * 100).toFixed(
188
+ 2
189
+ );
190
+ spinner.succeed(
191
+ `${chalk.red(beforeSize)} ${chalk.yellowBright(
192
+ "=>"
193
+ )} ${chalk.green(afterSize)} (${
194
+ offPercent >= 0 ? chalk.green("↓") : chalk.red("↑")
195
+ }${Math.abs(offPercent)}%)【 ${customFileName}${extname} 】`
196
+ );
197
+ // 收集压缩前后的文件名映射关系
198
+ collectReplaceMaps(replaceMaps, {
199
+ text: `${path.basename(file)}`,
200
+ newText: `${customFileName}${extname}`,
201
+ });
202
+ statisticsCount.ok++;
203
+ statisticsCount.okFiles.push(outputPath);
204
+ } else {
205
+ spinner.fail(
206
+ `不支持此文件类型[${filetype || fileName || file}]!`
207
+ );
208
+ statisticsCount.error++;
209
+ statisticsCount.okFiles.push(outputPath);
210
+ }
211
+ }
212
+ }
213
+ } catch (err) {
214
+ // console.log(`err`, err);
215
+ spinner.fail("出错啦, 文件不存在或不支持此类型 \n" + err);
216
+ statisticsCount.error++;
217
+ statisticsCount.errFiles.push(outputPath);
218
+ }
219
+ }
220
+
221
+ let statisticsCount = {
222
+ ok: 0,
223
+ okFiles: [],
224
+ err: 0,
225
+ errFiles: [],
226
+ };
227
+ // 指定了文件, 压缩
228
+ if (file) {
229
+ await tinyFile(file, statisticsCount);
230
+ }
231
+
232
+ // 指定文件夹, 翻译文件夹内所有文件
233
+ if (dir || condition) {
234
+ let inputDirPath = dir || "./";
235
+ let files;
236
+ try {
237
+ // 读取所有文件
238
+ files = fs.readdirSync(inputDirPath);
239
+ // spinner.succeed(`共有${files.length}个文件`);
240
+ } catch (err) {
241
+ spinner.fail("出错啦, 文件夹似乎不存在");
242
+ process.exit(1);
243
+ }
244
+
245
+ for (let i = 0; i < files.length; i++) {
246
+ let file = files[i];
247
+ let filePath = path.join(inputDirPath, file);
248
+ let stats = fs.statSync(filePath);
249
+ if (stats.isFile()) {
250
+ if (condition) {
251
+ let fileName = path.basename(file);
252
+ let index = fileName.indexOf(condition);
253
+ if (index > -1) {
254
+ let tip = setHighLightStr(fileName, condition);
255
+ spinner.succeed(`${tip}正在压缩`);
256
+ await tinyFile(filePath, statisticsCount, {
257
+ count: files.length,
258
+ order: i,
259
+ });
260
+ }
261
+ } else {
262
+ spinner.succeed(`正在压缩:${file}`);
263
+ await tinyFile(filePath, statisticsCount, {
264
+ count: files.length,
265
+ order: i,
266
+ });
267
+ }
268
+ } else {
269
+ spinner.fail(`${filePath}不是文件`);
270
+ }
271
+ }
272
+ spinner.succeed(`压缩成功 ${chalk.green(statisticsCount.ok)} 个`);
273
+ }
274
+
275
+ // 自动替换wiki链接
276
+ // 因为obsidian里已经有了一个插件, 可以自动上传到picgo并替换链接, 功能重复度很高
277
+ // 后续考虑给作者提个pr, 加入压缩功能
278
+ // console.log(`fileMaps`, replaceMaps);
279
+ if (option.replace) {
280
+ // 上传成功改的图片文件
281
+ let tinyFiles = statisticsCount.okFiles;
282
+ if (tinyFiles && tinyFiles.length) {
283
+ // await batchUploadByPicGo(tinyFiles, option);
284
+ if (!option.replace) process.exit(1);
285
+ spinner.start("开始替换文件内容, 替换后请仔细检查!");
286
+ console.log(`replaceMaps`, replaceMaps);
287
+ const fileContent = await replaceFileContent(
288
+ option.replaceFile,
289
+ replaceMaps
290
+ );
291
+ console.log(`fileContent`, fileContent);
292
+ if (!fileContent) {
293
+ spinner.fail("替换文件内容失败! 请检查要替换的文件是否存在!");
294
+ process.exit(1);
295
+ }
296
+ try {
297
+ fs.writeFileSync(option.replaceFile, fileContent, "utf-8");
298
+ spinner.succeed(
299
+ `[${chalk.red(
300
+ path.basename(option.replaceFile)
301
+ )}]图片链接替换完成!请前往检查!`
302
+ );
303
+ } catch (err) {
304
+ spinner.fail("图片链接替换失败!");
305
+ }
306
+ }
307
+ // 处理完后不再上传picgo
308
+ process.exit(1);
309
+ }
310
+ // 自动上传至picgo
311
+ if (option.picgo) {
312
+ spinner.start(`正在连接picgo`);
313
+ await picgoCmd.action({
314
+ tinyFiles: statisticsCount.okFiles,
315
+ max: option.max,
316
+ replace: isNeedUploadAndReplace,
317
+ replaceFile: option.replaceFile,
318
+ replaceMaps,
319
+ });
320
+ spinner.stop();
321
+
322
+ // 只有开启了picgo后时 才会使用替换功能 后续替换功能会独立出去 目前仅为满足自己所需
323
+ // if (option.replace && option.replaceFile) {
324
+ // spinner.start(`开始替换内容, 替换后请仔细检查!`);
325
+ // }
326
+ }
327
+ },
328
+ };