xw-devtool-cli 1.0.1 → 1.0.2
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/README.md +113 -64
- package/package.json +8 -4
- package/src/commands/imgConvert.js +112 -0
- package/src/index.js +5 -0
- package/src/utils/fileDialog.js +8 -7
package/README.md
CHANGED
|
@@ -1,73 +1,122 @@
|
|
|
1
1
|
# xw-devtool-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
一个基于 Node.js 的开发者命令行工具箱,旨在提供开箱即用的常用开发工具,帮助开发者快速处理日常任务。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- 全局安装:`npm i -g xw-devtool-cli`
|
|
7
|
-
- 启动:在终端运行 `xw-devtool`
|
|
8
|
-
- 免安装一次性使用:`npx xw-devtool@latest`
|
|
5
|
+
主要功能包括:Base64 编解码、图片格式转换、图片与 Base64 互转、Mock 数据生成、时间戳/日期格式化、URL 编解码、UUID 生成、汉字转拼音等。所有结果均自动复制到剪贴板,极大提升开发效率。
|
|
9
6
|
|
|
10
|
-
##
|
|
11
|
-
|
|
7
|
+
## ✨ 功能特性
|
|
8
|
+
|
|
9
|
+
- **Base64 工具**:支持字符串与 Base64 互转,支持从剪贴板、文件或手动输入读取。
|
|
10
|
+
- **图片工具**:
|
|
11
|
+
- **图片格式转换**:支持 PNG、JPG、WebP 格式互转,可调整压缩质量。
|
|
12
|
+
- **图片 ↔ Base64**:支持图片转 Base64 字符串,以及 Base64 还原为图片文件。
|
|
13
|
+
- **Mock 数据生成**:
|
|
14
|
+
- 支持生成:英文段落 (Lorem Ipsum)、中文字符、中国居民身份证号、电子邮箱、URL、订单号、手机号、座机号。
|
|
15
|
+
- 支持批量生成。
|
|
16
|
+
- **时间工具**:
|
|
17
|
+
- **时间格式化**:时间戳/日期字符串 -> `YYYY-MM-DD HH:mm:ss`。
|
|
18
|
+
- **获取时间戳**:快速获取当前毫秒级时间戳。
|
|
19
|
+
- **其他工具**:
|
|
20
|
+
- **URL 编解码**:Encode/Decode URL。
|
|
21
|
+
- **UUID**:生成 UUID v4。
|
|
22
|
+
- **中文转拼音**:将汉字转换为不带声调的拼音。
|
|
23
|
+
- **便捷操作**:
|
|
24
|
+
- 支持文件选择对话框 (Windows)。
|
|
25
|
+
- 结果自动复制到剪贴板。
|
|
26
|
+
- 支持将大文本结果保存为文件。
|
|
27
|
+
|
|
28
|
+
## 📦 安装
|
|
29
|
+
|
|
30
|
+
### 全局安装 (推荐)
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g xw-devtool-cli
|
|
12
33
|
```
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
6. Mock Text
|
|
19
|
-
7. Get UUID
|
|
20
|
-
8. Chinese to Pinyin
|
|
21
|
-
0. Exit
|
|
34
|
+
安装后即可在终端直接使用 `xw-devtool` 命令。
|
|
35
|
+
|
|
36
|
+
### 免安装运行 (npx)
|
|
37
|
+
```bash
|
|
38
|
+
npx xw-devtool-cli@latest
|
|
22
39
|
```
|
|
23
40
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
##
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
41
|
+
## 🚀 快速开始
|
|
42
|
+
|
|
43
|
+
在终端运行:
|
|
44
|
+
```bash
|
|
45
|
+
xw-devtool
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
启动后将显示交互式菜单,通过键盘方向键或输入数字选择功能:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
? Select a tool: (Use arrow keys)
|
|
52
|
+
> 1. URL Encode/Decode
|
|
53
|
+
2. String Encode/Decode (Base64)
|
|
54
|
+
3. Image <-> Base64
|
|
55
|
+
4. Image Format Convert
|
|
56
|
+
5. Time Format
|
|
57
|
+
6. Get Current Timestamp
|
|
58
|
+
7. Mock Text
|
|
59
|
+
8. Get UUID
|
|
60
|
+
9. Chinese to Pinyin
|
|
61
|
+
0. Exit
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 📖 详细使用指南
|
|
65
|
+
|
|
66
|
+
### 1. URL 编解码
|
|
67
|
+
- 选择 `1` 进入。
|
|
68
|
+
- 选择 `Encode` (编码) 或 `Decode` (解码)。
|
|
69
|
+
- 输入 URL 字符串,结果自动复制。
|
|
70
|
+
|
|
71
|
+
### 2. 字符串 Base64 转换
|
|
72
|
+
- 选择 `2` 进入。
|
|
73
|
+
- 支持三种输入源:
|
|
74
|
+
- **Clipboard**: 直接读取剪贴板内容(适合处理长文本)。
|
|
75
|
+
- **File**: 读取文本文件内容。
|
|
76
|
+
- **Manual input**: 手动粘贴或输入文本。
|
|
77
|
+
- 输出支持:直接复制、保存为文件、预览后复制。
|
|
78
|
+
|
|
79
|
+
### 3. 图片与 Base64 互转
|
|
80
|
+
- 选择 `3` 进入。
|
|
81
|
+
- **Image -> Base64**:
|
|
82
|
+
- 选择图片文件(支持对话框选择)。
|
|
83
|
+
- 输出 Base64 字符串(可保存为 `.txt` 文件防止控制台卡顿)。
|
|
84
|
+
- **Base64 -> Image**:
|
|
85
|
+
- 输入 Base64 字符串(支持从文件读取或剪贴板读取)。
|
|
86
|
+
- 自动识别图片格式并保存为文件。
|
|
87
|
+
|
|
88
|
+
### 4. 图片格式转换
|
|
89
|
+
- 选择 `4` 进入。
|
|
90
|
+
- 选择源图片文件。
|
|
91
|
+
- 选择目标格式 (PNG / JPG / WebP)。
|
|
92
|
+
- 设置压缩参数(如 JPG 质量 1-100,PNG 压缩等级 0-9)。
|
|
93
|
+
- 生成的新图片将保存在源文件同级目录。
|
|
94
|
+
|
|
95
|
+
### 5. Mock 数据生成
|
|
96
|
+
- 选择 `7` 进入。
|
|
97
|
+
- 提供多种数据类型:
|
|
98
|
+
- **英文段落**: 生成 Lorem Ipsum 文本。
|
|
99
|
+
- **中文字符**: 生成随机常用汉字。
|
|
100
|
+
- **身份证号**: 生成符合规则的虚拟身份证号 (包含校验位)。
|
|
101
|
+
- **邮箱**: 生成随机电子邮箱地址。
|
|
102
|
+
- **URL**: 生成随机网址。
|
|
103
|
+
- **订单号**: 生成基于时间戳的模拟订单号。
|
|
104
|
+
- **手机号**: 生成常见号段的手机号。
|
|
105
|
+
- **座机号**: 生成带区号的座机号码。
|
|
106
|
+
- 支持指定生成数量(批量生成)。
|
|
107
|
+
|
|
108
|
+
### 6. 时间工具
|
|
109
|
+
- **时间格式化 (5)**: 输入时间戳(秒/毫秒)或日期字符串,输出标准化格式。留空直接回车则使用当前时间。
|
|
110
|
+
- **当前时间戳 (6)**: 快速获取当前毫秒时间戳。
|
|
111
|
+
|
|
112
|
+
### 7. 其他
|
|
113
|
+
- **UUID (8)**: 生成一个 UUID v4。
|
|
114
|
+
- **中文转拼音 (9)**: 输入中文文本,输出拼音字符串(空格分隔)。
|
|
72
115
|
|
|
116
|
+
## 💻 系统要求
|
|
117
|
+
- Node.js >= 18.0.0
|
|
118
|
+
- Windows 用户可享受原生文件选择/保存对话框体验。
|
|
119
|
+
- macOS/Linux 用户支持完整功能,但文件路径需手动输入。
|
|
73
120
|
|
|
121
|
+
## 📄 License
|
|
122
|
+
ISC
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xw-devtool-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "基于node的开发者助手cli",
|
|
6
6
|
"main": "index.js",
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
"mock",
|
|
28
28
|
"timestamp",
|
|
29
29
|
"time",
|
|
30
|
-
"image"
|
|
30
|
+
"image",
|
|
31
|
+
"webp",
|
|
32
|
+
"jpg",
|
|
33
|
+
"png"
|
|
31
34
|
],
|
|
32
35
|
"author": "npmxw",
|
|
33
36
|
"license": "ISC",
|
|
@@ -36,10 +39,10 @@
|
|
|
36
39
|
},
|
|
37
40
|
"repository": {
|
|
38
41
|
"type": "git",
|
|
39
|
-
"url": "
|
|
42
|
+
"url": "https://gitee.com/github-9819409/xw-devtool-cli.git"
|
|
40
43
|
},
|
|
41
44
|
"bugs": {
|
|
42
|
-
"url": "https://
|
|
45
|
+
"url": "https://gitee.com/github-9819409/xw-devtool-cli/issues"
|
|
43
46
|
},
|
|
44
47
|
"dependencies": {
|
|
45
48
|
"clipboardy": "^5.0.2",
|
|
@@ -48,6 +51,7 @@
|
|
|
48
51
|
"inquirer": "^13.1.0",
|
|
49
52
|
"lorem-ipsum": "^2.0.8",
|
|
50
53
|
"pinyin": "^4.0.0",
|
|
54
|
+
"sharp": "^0.33.5",
|
|
51
55
|
"uuid": "^13.0.0"
|
|
52
56
|
}
|
|
53
57
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import { selectFromMenu } from '../utils/menu.js';
|
|
6
|
+
import { selectFile, saveFile } from '../utils/fileDialog.js';
|
|
7
|
+
import { defaultFileName } from '../utils/output.js';
|
|
8
|
+
|
|
9
|
+
function getExt(p) {
|
|
10
|
+
const ext = path.extname(p).toLowerCase().replace('.', '');
|
|
11
|
+
if (ext === 'jpeg') return 'jpg';
|
|
12
|
+
return ext;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function suggestOutputPath(inputPath, targetExt) {
|
|
16
|
+
const dir = path.dirname(inputPath);
|
|
17
|
+
const base = path.basename(inputPath, path.extname(inputPath));
|
|
18
|
+
return path.join(dir, `${base}.${targetExt}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function pickInputFile() {
|
|
22
|
+
const method = await selectFromMenu('File input method', [
|
|
23
|
+
{ name: 'Select file (dialog)', value: 'dialog' },
|
|
24
|
+
{ name: 'Enter file path manually', value: 'manual' }
|
|
25
|
+
]);
|
|
26
|
+
let filePath = '';
|
|
27
|
+
if (method === 'dialog') {
|
|
28
|
+
filePath = selectFile('Image Files|*.png;*.jpg;*.jpeg;*.webp|All Files|*.*') || '';
|
|
29
|
+
if (!filePath) {
|
|
30
|
+
console.log('File dialog not available or canceled. Please enter path manually.');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!filePath) {
|
|
34
|
+
const ans = await inquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: 'input',
|
|
37
|
+
name: 'filePath',
|
|
38
|
+
message: 'Enter image file path:',
|
|
39
|
+
validate: (input) => (fs.existsSync(input) ? true : 'File does not exist.')
|
|
40
|
+
}
|
|
41
|
+
]);
|
|
42
|
+
filePath = ans.filePath;
|
|
43
|
+
}
|
|
44
|
+
return filePath;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function pickTargetFormat(currentExt) {
|
|
48
|
+
const options = [
|
|
49
|
+
{ name: 'png', value: 'png' },
|
|
50
|
+
{ name: 'jpg', value: 'jpg' },
|
|
51
|
+
{ name: 'webp', value: 'webp' }
|
|
52
|
+
].filter(o => o.value !== currentExt);
|
|
53
|
+
return await selectFromMenu('Target format', options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function pickQuality(targetExt) {
|
|
57
|
+
if (targetExt === 'png') {
|
|
58
|
+
const { level } = await inquirer.prompt([
|
|
59
|
+
{ type: 'number', name: 'level', message: 'PNG compression level (0-9):', default: 6, validate: n => (n >= 0 && n <= 9) ? true : '0-9' }
|
|
60
|
+
]);
|
|
61
|
+
return { pngCompressionLevel: level };
|
|
62
|
+
} else if (targetExt === 'jpg') {
|
|
63
|
+
const { quality } = await inquirer.prompt([
|
|
64
|
+
{ type: 'number', name: 'quality', message: 'JPEG quality (1-100):', default: 80, validate: n => (n >= 1 && n <= 100) ? true : '1-100' }
|
|
65
|
+
]);
|
|
66
|
+
return { jpegQuality: quality };
|
|
67
|
+
} else if (targetExt === 'webp') {
|
|
68
|
+
const { quality } = await inquirer.prompt([
|
|
69
|
+
{ type: 'number', name: 'quality', message: 'WebP quality (1-100):', default: 80, validate: n => (n >= 1 && n <= 100) ? true : '1-100' }
|
|
70
|
+
]);
|
|
71
|
+
return { webpQuality: quality };
|
|
72
|
+
}
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function imgConvertHandler() {
|
|
77
|
+
const inputPath = await pickInputFile();
|
|
78
|
+
const inputExt = getExt(inputPath);
|
|
79
|
+
console.log(`Input: ${inputPath}`);
|
|
80
|
+
|
|
81
|
+
const targetExt = await pickTargetFormat(inputExt);
|
|
82
|
+
console.log(`Target format: ${targetExt}`);
|
|
83
|
+
const opts = await pickQuality(targetExt);
|
|
84
|
+
|
|
85
|
+
let outPath = saveFile(suggestOutputPath(inputPath, targetExt), 'Image Files|*.png;*.jpg;*.jpeg;*.webp|All Files|*.*');
|
|
86
|
+
if (!outPath) {
|
|
87
|
+
const def = suggestOutputPath(inputPath, targetExt);
|
|
88
|
+
const ans = await inquirer.prompt([
|
|
89
|
+
{ type: 'input', name: 'outPath', message: 'Enter output file path:', default: def }
|
|
90
|
+
]);
|
|
91
|
+
outPath = ans.outPath;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
96
|
+
let pipeline = sharp(inputPath);
|
|
97
|
+
if (targetExt === 'png') {
|
|
98
|
+
pipeline = pipeline.png({ compressionLevel: opts.pngCompressionLevel ?? 6 });
|
|
99
|
+
} else if (targetExt === 'jpg') {
|
|
100
|
+
pipeline = pipeline.jpeg({ quality: opts.jpegQuality ?? 80 });
|
|
101
|
+
if (inputExt !== 'jpg') {
|
|
102
|
+
pipeline = pipeline.flatten({ background: { r: 255, g: 255, b: 255 } });
|
|
103
|
+
}
|
|
104
|
+
} else if (targetExt === 'webp') {
|
|
105
|
+
pipeline = pipeline.webp({ quality: opts.webpQuality ?? 80 });
|
|
106
|
+
}
|
|
107
|
+
await pipeline.toFile(outPath);
|
|
108
|
+
console.log(`Converted: ${inputPath} -> ${outPath}`);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error('Conversion error:', e.message);
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { program } from 'commander';
|
|
|
3
3
|
import { urlHandler } from './commands/url.js';
|
|
4
4
|
import { base64Handler } from './commands/base64.js';
|
|
5
5
|
import { imgBase64Handler } from './commands/imgBase64.js';
|
|
6
|
+
import { imgConvertHandler } from './commands/imgConvert.js';
|
|
6
7
|
import { timeFormatHandler } from './commands/timeFormat.js';
|
|
7
8
|
import { timestampHandler } from './commands/timestamp.js';
|
|
8
9
|
import { mockHandler } from './commands/mock.js';
|
|
@@ -28,6 +29,7 @@ const features = [
|
|
|
28
29
|
{ name: 'URL Encode/Decode', value: 'url' },
|
|
29
30
|
{ name: 'String Encode/Decode (Base64)', value: 'base64' },
|
|
30
31
|
{ name: 'Image <-> Base64', value: 'imgBase64' },
|
|
32
|
+
{ name: 'Image Format Convert', value: 'imgConvert' },
|
|
31
33
|
{ name: 'Time Format', value: 'timeFormat' },
|
|
32
34
|
{ name: 'Get Current Timestamp', value: 'timestamp' },
|
|
33
35
|
{ name: 'Mock Text', value: 'mock' },
|
|
@@ -102,6 +104,9 @@ async function handleAction(action) {
|
|
|
102
104
|
case 'imgBase64':
|
|
103
105
|
await imgBase64Handler();
|
|
104
106
|
break;
|
|
107
|
+
case 'imgConvert':
|
|
108
|
+
await imgConvertHandler();
|
|
109
|
+
break;
|
|
105
110
|
case 'timeFormat':
|
|
106
111
|
await timeFormatHandler();
|
|
107
112
|
break;
|
package/src/utils/fileDialog.js
CHANGED
|
@@ -7,9 +7,10 @@ function isWindows() {
|
|
|
7
7
|
export function selectFile(filter = 'All Files|*.*') {
|
|
8
8
|
if (!isWindows()) return null;
|
|
9
9
|
try {
|
|
10
|
-
const cmd = `powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $fd = New-Object System.Windows.Forms.OpenFileDialog; $fd.Filter='${filter.replace(/"/g, '\\"')}'; $fd.Multiselect=$false; if($fd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){
|
|
11
|
-
const
|
|
12
|
-
|
|
10
|
+
const cmd = `powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $fd = New-Object System.Windows.Forms.OpenFileDialog; $fd.Filter='${filter.replace(/"/g, '\\"')}'; $fd.Multiselect=$false; if($fd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){ $bytes = [System.Text.Encoding]::UTF8.GetBytes($fd.FileName); Write-Output ([Convert]::ToBase64String($bytes)) }"`;
|
|
11
|
+
const buf = execSync(cmd, { stdio: ['pipe', 'pipe', 'ignore'] });
|
|
12
|
+
const output = Buffer.isBuffer(buf) ? buf.toString('utf8').trim() : String(buf).trim();
|
|
13
|
+
return output ? Buffer.from(output, 'base64').toString('utf8') : null;
|
|
13
14
|
} catch {
|
|
14
15
|
return null;
|
|
15
16
|
}
|
|
@@ -18,11 +19,11 @@ export function selectFile(filter = 'All Files|*.*') {
|
|
|
18
19
|
export function saveFile(defaultName = 'output.txt', filter = 'All Files|*.*') {
|
|
19
20
|
if (!isWindows()) return null;
|
|
20
21
|
try {
|
|
21
|
-
const cmd = `powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $sd = New-Object System.Windows.Forms.SaveFileDialog; $sd.Filter='${filter.replace(/"/g, '\\"')}'; $sd.FileName='${defaultName.replace(/"/g, '\\"')}'; if($sd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const cmd = `powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $sd = New-Object System.Windows.Forms.SaveFileDialog; $sd.Filter='${filter.replace(/"/g, '\\"')}'; $sd.FileName='${defaultName.replace(/"/g, '\\"')}'; if($sd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){ $bytes = [System.Text.Encoding]::UTF8.GetBytes($sd.FileName); Write-Output ([Convert]::ToBase64String($bytes)) }"`;
|
|
23
|
+
const buf = execSync(cmd, { stdio: ['pipe', 'pipe', 'ignore'] });
|
|
24
|
+
const output = Buffer.isBuffer(buf) ? buf.toString('utf8').trim() : String(buf).trim();
|
|
25
|
+
return output ? Buffer.from(output, 'base64').toString('utf8') : null;
|
|
24
26
|
} catch {
|
|
25
27
|
return null;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
|
-
|