@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.
- package/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/package.json +47 -0
- package/src/command/config.js +97 -0
- package/src/command/index.js +26 -0
- package/src/command/picgo.js +122 -0
- package/src/command/replace.js +81 -0
- package/src/command/set.js +57 -0
- package/src/command/tiny.js +328 -0
- package/src/command/translate.js +334 -0
- package/src/config.json +16 -0
- package/src/index.js +26 -0
- package/src/translate-api/index.js +47 -0
- package/src/translate-api/md5.js +231 -0
- package/src/utils/common.js +200 -0
- package/src/utils/file.js +61 -0
- package/src/utils/picgo.js +149 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import latestVersion from 'latest-version';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
export const defaultConfig = {
|
|
14
|
+
"translate": {
|
|
15
|
+
"sourceDirName": "zh-CN",
|
|
16
|
+
"targetDirName": "en-US",
|
|
17
|
+
"ignoreFiles": ["node_modules"],
|
|
18
|
+
"account": {
|
|
19
|
+
"appId": "",
|
|
20
|
+
"key": ""
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"editableConfig": ["translate"]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function getLocalConfig() {
|
|
27
|
+
let spinner = ora();
|
|
28
|
+
const homeDir = os.homedir();
|
|
29
|
+
const configDir = path.join(homeDir, '.zzclub-z-cli');
|
|
30
|
+
const configPath = path.join(configDir, 'config.json');
|
|
31
|
+
let config = {}
|
|
32
|
+
if (fs.existsSync(configPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
35
|
+
config = JSON.parse(content);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
spinner.fail('读取配置文件失败:' + JSON.stringify(err));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
spinner.warn('未找到配置文件,正在初始化默认配置');
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
config = setLocalConfig(defaultConfig, spinner);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 写入文件
|
|
53
|
+
* @param filePath 文件路径
|
|
54
|
+
* @param fileContent 文件内容
|
|
55
|
+
* @param onFinally 完成时回调
|
|
56
|
+
*/
|
|
57
|
+
export function writeFileContent(filePath, fileContent, onFinally = () => {}) {
|
|
58
|
+
let spinner = ora();
|
|
59
|
+
fs.writeFile(filePath, fileContent, "utf-8", (error) => {
|
|
60
|
+
if (!error) {
|
|
61
|
+
// spinner.succeed(`已写入${chalk.yellow(filePath)}`);
|
|
62
|
+
onFinally && onFinally(spinner, true);
|
|
63
|
+
} else {
|
|
64
|
+
console.log(error);
|
|
65
|
+
// spinner.fail(`写入${chalk.red(filePath)}文件失败, 请重试`);
|
|
66
|
+
onFinally && onFinally(spinner, false);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const readJsonFile = async (filePath) => {
|
|
72
|
+
let jsonData;
|
|
73
|
+
try {
|
|
74
|
+
let jsonStr = fs.readFileSync(filePath);
|
|
75
|
+
jsonData = JSON.parse(jsonStr.toString());
|
|
76
|
+
} catch (err) {
|
|
77
|
+
jsonData = {};
|
|
78
|
+
}
|
|
79
|
+
return jsonData;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const setHighLightStr = (
|
|
83
|
+
sourceText,
|
|
84
|
+
hightlightText,
|
|
85
|
+
chalkFn = chalk.red
|
|
86
|
+
) => {
|
|
87
|
+
let index = sourceText.indexOf(hightlightText);
|
|
88
|
+
if (index === -1) return sourceText;
|
|
89
|
+
return sourceText.replaceAll(hightlightText, chalkFn(hightlightText));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
//
|
|
93
|
+
export async function setLocalConfig(newConfig = {}, spinner) {
|
|
94
|
+
const homeDir = os.homedir();
|
|
95
|
+
const configDir = path.join(homeDir, '.zzclub-z-cli');
|
|
96
|
+
const configPath = path.join(configDir, 'config.json');
|
|
97
|
+
|
|
98
|
+
// 确保配置目录存在
|
|
99
|
+
if (!fs.existsSync(configDir)) {
|
|
100
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 读取已存在的配置
|
|
104
|
+
let config = {};
|
|
105
|
+
if (fs.existsSync(configPath)) {
|
|
106
|
+
try {
|
|
107
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
108
|
+
config = JSON.parse(content);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
spinner && spinner.fail('读取配置文件失败')
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
config = {
|
|
116
|
+
...config,
|
|
117
|
+
...newConfig,
|
|
118
|
+
translate: {
|
|
119
|
+
...config.translate,
|
|
120
|
+
...newConfig.translate,
|
|
121
|
+
account: {
|
|
122
|
+
...(config.translate?.account || {}),
|
|
123
|
+
...newConfig.translate.account,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// 保存配置
|
|
129
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
130
|
+
spinner && spinner.succeed(`配置文件已更新:${chalk.yellow(configPath)}`)
|
|
131
|
+
return config;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
// 获取 package.json
|
|
136
|
+
export function getPackageJson() {
|
|
137
|
+
try {
|
|
138
|
+
const rootDir = path.resolve(__dirname, '../../');
|
|
139
|
+
const packagePath = path.join(rootDir, 'package.json');
|
|
140
|
+
const content = fs.readFileSync(packagePath, 'utf8');
|
|
141
|
+
return JSON.parse(content);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error('读取 package.json 失败:', err);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
// 检查版本更新
|
|
150
|
+
// ... 其他代码保持不变 ...
|
|
151
|
+
|
|
152
|
+
// 检查版本更新
|
|
153
|
+
export async function checkUpdate() {
|
|
154
|
+
// 获取缓存目录
|
|
155
|
+
const homeDir = os.homedir();
|
|
156
|
+
const cacheDir = path.join(homeDir, '.zzclub-z-cli');
|
|
157
|
+
const cacheFile = path.join(cacheDir, 'version-cache.json');
|
|
158
|
+
|
|
159
|
+
// 检查是否需要更新
|
|
160
|
+
const needCheck = () => {
|
|
161
|
+
try {
|
|
162
|
+
if (!fs.existsSync(cacheFile)) return true;
|
|
163
|
+
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
164
|
+
// 24小时内不重复检查
|
|
165
|
+
return Date.now() - cache.lastCheck > 24 * 60 * 60 * 1000;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if (!needCheck()) return;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const pkg = getPackageJson();
|
|
176
|
+
if (!pkg) return;
|
|
177
|
+
let spinner = ora();
|
|
178
|
+
spinner.start('正在检测版本更新...')
|
|
179
|
+
const latest = await latestVersion('@zzclub/z-cli');
|
|
180
|
+
if (latest !== pkg.version) {
|
|
181
|
+
spinner.warn(chalk.yellow(`\n提示: 发现新版本 ${chalk.green(latest)},当前版本 ${chalk.red(pkg.version)}\n`))
|
|
182
|
+
} else {
|
|
183
|
+
spinner.succeed('当前为最新版本!')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 更新缓存
|
|
187
|
+
if (!fs.existsSync(cacheDir)) {
|
|
188
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
189
|
+
}
|
|
190
|
+
fs.writeFileSync(cacheFile, JSON.stringify({
|
|
191
|
+
lastCheck: Date.now(),
|
|
192
|
+
version: latest
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
spinner.stop();
|
|
196
|
+
|
|
197
|
+
} catch (err) {
|
|
198
|
+
// 静默处理错误
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
// 导出一个异步函数,用于读取JSON文件
|
|
3
|
+
export const readJsonFile = async (filePath) => {
|
|
4
|
+
let jsonData;
|
|
5
|
+
try {
|
|
6
|
+
// 使用fs.readFileSync读取文件内容
|
|
7
|
+
let jsonStr = fs.readFileSync(filePath);
|
|
8
|
+
// 将文件内容转换为JSON对象
|
|
9
|
+
jsonData = JSON.parse(jsonStr);
|
|
10
|
+
} catch (err) {
|
|
11
|
+
// 如果读取文件失败,则将jsonData初始化为空对象
|
|
12
|
+
jsonData = {};
|
|
13
|
+
}
|
|
14
|
+
// 返回JSON对象
|
|
15
|
+
return jsonData;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// 用于获取格式化的文件大小
|
|
19
|
+
export const getFormatedFileSize = (byte) => {
|
|
20
|
+
if (byte === 0) return "0 Bytes";
|
|
21
|
+
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
22
|
+
const i = parseInt(Math.floor(Math.log(byte) / Math.log(1024)), 10);
|
|
23
|
+
if (i === 0) return byte + " " + sizes[i];
|
|
24
|
+
return (byte / Math.pow(1024, i)).toFixed(1) + " " + sizes[i];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// 检测文件是否存在 且 是文件类型
|
|
28
|
+
export const checkFileExist = (filePath) => {
|
|
29
|
+
try {
|
|
30
|
+
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// 获取文件信息, 没返回说明文件不存在, 或不是一个文件
|
|
37
|
+
export const getFileInfo = (filePath) => {
|
|
38
|
+
let fileInfo = {};
|
|
39
|
+
if (checkFileExist(filePath)) {
|
|
40
|
+
fileInfo.fileName = filePath.split("/").pop();
|
|
41
|
+
fileInfo.formatFileSize = getFormatedFileSize(fs.statSync(filePath).size);
|
|
42
|
+
fileInfo.rawFileSize = fs.statSync(filePath).size;
|
|
43
|
+
fileInfo.fileType = filePath.split(".").pop();
|
|
44
|
+
}
|
|
45
|
+
return fileInfo;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// 替换文件内的内容
|
|
49
|
+
// replaceMaps: [ {text: '1', newText: '2'}, {text: '45', newText: '112'},]
|
|
50
|
+
export const replaceFileContent = async (file, replaceMaps) => {
|
|
51
|
+
try {
|
|
52
|
+
let fileContent = fs.readFileSync(file, "utf8");
|
|
53
|
+
replaceMaps.forEach((item) => {
|
|
54
|
+
if (item.text && item.newText)
|
|
55
|
+
fileContent = fileContent.replaceAll(item.text, item.newText);
|
|
56
|
+
});
|
|
57
|
+
return fileContent;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { getFileInfo } from "./file.js";
|
|
5
|
+
import sharp from "sharp";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
// 检查文件是否超过最大值, 或不满足最小值
|
|
8
|
+
export const checkFileSize = (filePath, { max, min }) => {
|
|
9
|
+
const fileInfo = getFileInfo(filePath);
|
|
10
|
+
if (!fileInfo.fileName) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
msg: "文件不存在",
|
|
14
|
+
fileInfo: {},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const { rawFileSize: fileSize, fileName } = fileInfo;
|
|
18
|
+
const kbValue = fileSize / 1024;
|
|
19
|
+
if (max && kbValue > max) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
msg: `文件[${fileName}](${kbValue.toFixed(1)}kb)大小超过${max}kb`,
|
|
23
|
+
fileInfo,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (min && kbValue < min) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
msg: `文件[${fileName}](${kbValue.toFixed(1)}kb)大小低于${min}kb`,
|
|
30
|
+
fileInfo,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
msg: "ok",
|
|
37
|
+
fileInfo,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// 检测所有文件, 并过滤出可以被上传的文件
|
|
42
|
+
export const checkFileSizeAndFilter = (filePaths, { max, min }) => {
|
|
43
|
+
const okFiles = [],
|
|
44
|
+
errFiles = [];
|
|
45
|
+
if (filePaths && filePaths.length) {
|
|
46
|
+
filePaths.forEach((filePath) => {
|
|
47
|
+
let checkInfo = checkFileSize(filePath, { max, min });
|
|
48
|
+
// 是一个文件, 并且可以被sharp处理, 则上传
|
|
49
|
+
if (checkInfo.ok) {
|
|
50
|
+
if (sharp(filePath)[checkInfo.fileInfo.fileType]) {
|
|
51
|
+
okFiles.push(filePath);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
errFiles.push({
|
|
55
|
+
errFile: filePath,
|
|
56
|
+
fileInfo: checkInfo.fileInfo,
|
|
57
|
+
errMsg: checkInfo.msg,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
okFiles,
|
|
64
|
+
errFiles,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 请求picgo
|
|
69
|
+
export const requestPicGo = async (files) => {
|
|
70
|
+
let spinner = ora();
|
|
71
|
+
if (!files || !files.length) {
|
|
72
|
+
spinner.fail(`上传失败: 未指定文件`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
spinner.start("正在上传");
|
|
77
|
+
let picgoRes = await axios
|
|
78
|
+
.post("http://127.0.0.1:36677/upload", {
|
|
79
|
+
list: files,
|
|
80
|
+
})
|
|
81
|
+
.catch((err) => {
|
|
82
|
+
spinner.fail("上传失败!请启动PicGo后重试!");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
85
|
+
// 成功
|
|
86
|
+
if (picgoRes.data && picgoRes.data.success) {
|
|
87
|
+
spinner.succeed(`上传成功 ${chalk.green(picgoRes.data.result.length)} 个!`);
|
|
88
|
+
// 返回上传后的url列表
|
|
89
|
+
return picgoRes.data.result;
|
|
90
|
+
} else {
|
|
91
|
+
spinner.fail(`上传失败: ${picgoRes.data.message}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// 上传文件
|
|
97
|
+
export const uploadFileByPicGo = async (file, option) => {
|
|
98
|
+
const spinner = ora();
|
|
99
|
+
let filePth = path.resolve(process.cwd(), file);
|
|
100
|
+
let checkResult = checkFileSize(filePth, { max: option.max });
|
|
101
|
+
if (checkResult.ok) {
|
|
102
|
+
let fileUrls = await requestPicGo([filePth]);
|
|
103
|
+
spinner.succeed(`上传地址: \n ${fileUrls.join("\n")}`);
|
|
104
|
+
} else {
|
|
105
|
+
spinner.fail(`上传失败: ${checkResult.msg}`);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// 批量上传文件
|
|
110
|
+
export const batchUploadByPicGo = async (files, option) => {
|
|
111
|
+
const spinner = ora();
|
|
112
|
+
const { okFiles, errFiles } = checkFileSizeAndFilter(files, {
|
|
113
|
+
max: option.max,
|
|
114
|
+
});
|
|
115
|
+
if (!okFiles.length) {
|
|
116
|
+
spinner.fail(
|
|
117
|
+
`该目录下没有${
|
|
118
|
+
option.condition ? "名称含有[" + option.condition + "]的" : ""
|
|
119
|
+
}图片文件(<=${option.max}kb)可以上传`
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
const fileUrls = await requestPicGo(okFiles);
|
|
124
|
+
// 如果要替换, 重新组装map
|
|
125
|
+
if (option.replace) {
|
|
126
|
+
option.replaceMaps = option.replaceMaps.map((item) => {
|
|
127
|
+
// obsidian里的文件名带空格, 所以需要编码一下
|
|
128
|
+
// 因为上传后的url里空格被编码了
|
|
129
|
+
let uploadedUrl = fileUrls.find(
|
|
130
|
+
(url) => url.indexOf(encodeURIComponent(item.newText)) !== -1
|
|
131
|
+
);
|
|
132
|
+
return {
|
|
133
|
+
text: item.text,
|
|
134
|
+
newText: uploadedUrl ? `` : "",
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
spinner.succeed(`上传地址: \n${fileUrls.join("\n")}`);
|
|
139
|
+
let realErrFiles = errFiles.filter(
|
|
140
|
+
(item) => item.fileInfo && item.fileInfo.fileName
|
|
141
|
+
);
|
|
142
|
+
if (realErrFiles.length) {
|
|
143
|
+
spinner.fail(
|
|
144
|
+
`未上传 ${chalk.red(realErrFiles.length)} 个: \n${realErrFiles
|
|
145
|
+
.map((item) => item.errMsg)
|
|
146
|
+
.join("\n")}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
};
|