xw-devtool-cli 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/README.md +73 -0
- package/bin/index.js +2 -0
- package/package.json +53 -0
- package/src/commands/base64.js +108 -0
- package/src/commands/imgBase64.js +164 -0
- package/src/commands/mock.js +60 -0
- package/src/commands/pinyin.js +25 -0
- package/src/commands/timeFormat.js +41 -0
- package/src/commands/timestamp.js +7 -0
- package/src/commands/url.js +31 -0
- package/src/commands/uuid.js +8 -0
- package/src/index.js +125 -0
- package/src/utils/clipboard.js +10 -0
- package/src/utils/fileDialog.js +28 -0
- package/src/utils/menu.js +32 -0
- package/src/utils/output.js +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# xw-devtool-cli
|
|
2
|
+
|
|
3
|
+
一个上手即用的命令行工具箱,帮你快速完成常见开发小任务:URL 编解码、字符串 Base64、图片与 Base64 互转、时间格式化与当前时间戳、Mock 文本(中/英)、UUID、中文转拼音等,且工具持续添加中......,结果会自动复制到剪贴板,方便粘贴到代码或文档。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
- 全局安装:`npm i -g xw-devtool-cli`
|
|
7
|
+
- 启动:在终端运行 `xw-devtool`
|
|
8
|
+
- 免安装一次性使用:`npx xw-devtool@latest`
|
|
9
|
+
|
|
10
|
+
## 快速开始
|
|
11
|
+
启动后显示主菜单,输入数字选择功能:
|
|
12
|
+
```
|
|
13
|
+
1. URL Encode/Decode
|
|
14
|
+
2. String Encode/Decode (Base64)
|
|
15
|
+
3. Image <-> Base64
|
|
16
|
+
4. Time Format
|
|
17
|
+
5. Get Current Timestamp
|
|
18
|
+
6. Mock Text
|
|
19
|
+
7. Get UUID
|
|
20
|
+
8. Chinese to Pinyin
|
|
21
|
+
0. Exit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 常用操作
|
|
25
|
+
- URL 编解码
|
|
26
|
+
- 选择 `1` → 选择 `Encode` 或 `Decode` → 输入 URL → 结果自动复制
|
|
27
|
+
- 字符串 Base64
|
|
28
|
+
- 选择 `2` → 选择 `Encode` 或 `Decode`
|
|
29
|
+
- 选择输入来源:`Clipboard`(推荐长文本)/ `File` / `Manual input`
|
|
30
|
+
- 选择输出方式:`Copy to clipboard` / `Save to file` / `Preview + copy`
|
|
31
|
+
- 保存文件会给出默认文件名:`base64-[encode|decode]-<timestamp>.txt`
|
|
32
|
+
- 图片 ↔ Base64
|
|
33
|
+
- 图片 → Base64:选择 `3` → 选择 `Image -> Base64` → 选择图片(对话框或手动路径)→ 选择输出方式(复制或保存为 `.txt`)
|
|
34
|
+
- Base64 → 图片:选择 `3` → 选择 `Base64 -> Image` → 选择输入来源(剪贴板、文件或手动输入)→ 自动识别 `data:image/*` 前缀并推断扩展名 → 保存图片(默认名:`image-<timestamp>.<ext>`)
|
|
35
|
+
- 时间工具
|
|
36
|
+
- 时间格式化:选择 `4` → 输入时间戳(秒/毫秒)或日期字符串,或直接回车使用当前时间 → 输出 `YYYY-MM-DD HH:mm:ss`
|
|
37
|
+
- 当前时间戳:选择 `5` → 显示毫秒级时间戳并复制
|
|
38
|
+
- Mock 文本
|
|
39
|
+
- 选择 `6` → 选择语言(英文/中文)
|
|
40
|
+
- 英文:输入段落数生成 Lorem Ipsum
|
|
41
|
+
- 中文:输入字数生成随机常用汉字
|
|
42
|
+
- UUID
|
|
43
|
+
- 选择 `7` → 生成 UUID v4 并复制
|
|
44
|
+
- 中文转拼音
|
|
45
|
+
- 选择 `8` → 输入中文 → 输出不带声调的拼音(空格分词)
|
|
46
|
+
|
|
47
|
+
## 输入与输出
|
|
48
|
+
- 输入来源
|
|
49
|
+
- `Clipboard`:直接读取剪贴板(适合非常长的内容)
|
|
50
|
+
- `File`:支持文件对话框或手动输入路径
|
|
51
|
+
- `Manual input`:命令行输入(适合短文本)
|
|
52
|
+
- 文件输入方式(当选择 `File`)
|
|
53
|
+
- `Select file (dialog)`:弹出系统文件选择框(Windows)
|
|
54
|
+
- `Enter file path manually`:手动输入路径
|
|
55
|
+
- 输出方式
|
|
56
|
+
- `Copy to clipboard`:不打印全文,直接复制
|
|
57
|
+
- `Save to file`:弹出保存对话框(Windows),或手动输入路径;默认文件名为“工具名 + 时间戳”
|
|
58
|
+
- `Preview + copy`:显示长度与头尾少量字符预览,并复制完整结果
|
|
59
|
+
|
|
60
|
+
## 提示
|
|
61
|
+
- 粘贴超长文本到控制台会很慢,建议使用 `Clipboard` 或 `File` 作为输入来源,并使用 `Save to file` 或 `Preview + copy` 作为输出方式
|
|
62
|
+
- 在任何步骤按 `Ctrl+C` 会优雅退出并显示 `Bye!`
|
|
63
|
+
- 工具在本地离线运行,不依赖网络
|
|
64
|
+
|
|
65
|
+
## 系统要求
|
|
66
|
+
- Node.js `>= 18`
|
|
67
|
+
- Windows:支持文件选择/保存对话框
|
|
68
|
+
- macOS/Linux:使用手动路径输入(可按需扩展对话框支持)
|
|
69
|
+
|
|
70
|
+
## 卸载
|
|
71
|
+
- 全局卸载:`npm uninstall -g xw-devtool-cli`
|
|
72
|
+
|
|
73
|
+
|
package/bin/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xw-devtool-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "基于node的开发者助手cli",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"xw-devtool": "./bin/index.js",
|
|
9
|
+
"xw-dev": "./bin/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node bin/index.js",
|
|
18
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"cli",
|
|
22
|
+
"devtools",
|
|
23
|
+
"base64",
|
|
24
|
+
"url",
|
|
25
|
+
"uuid",
|
|
26
|
+
"pinyin",
|
|
27
|
+
"mock",
|
|
28
|
+
"timestamp",
|
|
29
|
+
"time",
|
|
30
|
+
"image"
|
|
31
|
+
],
|
|
32
|
+
"author": "npmxw",
|
|
33
|
+
"license": "ISC",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://example.com/xw-devtool-cli.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://example.com/xw-devtool-cli/issues"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"clipboardy": "^5.0.2",
|
|
46
|
+
"commander": "^14.0.2",
|
|
47
|
+
"dayjs": "^1.11.19",
|
|
48
|
+
"inquirer": "^13.1.0",
|
|
49
|
+
"lorem-ipsum": "^2.0.8",
|
|
50
|
+
"pinyin": "^4.0.0",
|
|
51
|
+
"uuid": "^13.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import clipboardy from 'clipboardy';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { selectFromMenu } from '../utils/menu.js';
|
|
5
|
+
import { outputText, defaultFileName } from '../utils/output.js';
|
|
6
|
+
import { selectFile, saveFile } from '../utils/fileDialog.js';
|
|
7
|
+
|
|
8
|
+
export async function base64Handler() {
|
|
9
|
+
const mode = await selectFromMenu('Base64 Encode/Decode', [
|
|
10
|
+
{ name: 'Encode (String -> Base64)', value: 'encode' },
|
|
11
|
+
{ name: 'Decode (Base64 -> String)', value: 'decode' }
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const source = await selectFromMenu('Input Source', [
|
|
15
|
+
{ name: 'Clipboard', value: 'clip' },
|
|
16
|
+
{ name: 'File', value: 'file' },
|
|
17
|
+
{ name: 'Manual input', value: 'manual' }
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
let input = '';
|
|
21
|
+
if (source === 'clip') {
|
|
22
|
+
try {
|
|
23
|
+
input = await clipboardy.read();
|
|
24
|
+
if (!input) {
|
|
25
|
+
console.log('Clipboard is empty.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error('Failed to read clipboard:', e.message);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
} else if (source === 'file') {
|
|
33
|
+
const method = await selectFromMenu('File input method', [
|
|
34
|
+
{ name: 'Select file (dialog)', value: 'dialog' },
|
|
35
|
+
{ name: 'Enter file path manually', value: 'manual' }
|
|
36
|
+
]);
|
|
37
|
+
let p = '';
|
|
38
|
+
if (method === 'dialog') {
|
|
39
|
+
p = selectFile('Text Files|*.txt;*.log;*.md|All Files|*.*') || '';
|
|
40
|
+
if (!p) {
|
|
41
|
+
console.log('File dialog not available or canceled. Please enter path manually.');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!p) {
|
|
45
|
+
const ans = await inquirer.prompt([
|
|
46
|
+
{
|
|
47
|
+
type: 'input',
|
|
48
|
+
name: 'path',
|
|
49
|
+
message: 'Enter input file path:',
|
|
50
|
+
validate: (x) => (fs.existsSync(x) ? true : 'File does not exist.')
|
|
51
|
+
}
|
|
52
|
+
]);
|
|
53
|
+
p = ans.path;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
input = fs.readFileSync(p, 'utf-8');
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error('Failed to read file:', e.message);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const { text } = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: 'input',
|
|
65
|
+
name: 'text',
|
|
66
|
+
message: `Enter string to ${mode}:`
|
|
67
|
+
}
|
|
68
|
+
]);
|
|
69
|
+
input = text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
if (mode === 'encode') {
|
|
75
|
+
result = Buffer.from(input).toString('base64');
|
|
76
|
+
} else {
|
|
77
|
+
result = Buffer.from(input, 'base64').toString('utf-8');
|
|
78
|
+
}
|
|
79
|
+
const outputMethod = await selectFromMenu('Output', [
|
|
80
|
+
{ name: 'Copy to clipboard', value: 'copy' },
|
|
81
|
+
{ name: 'Save to file', value: 'file' },
|
|
82
|
+
{ name: 'Preview + copy', value: 'preview' }
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
if (outputMethod === 'file') {
|
|
86
|
+
const def = defaultFileName(mode === 'encode' ? 'base64-encode' : 'base64-decode', 'txt');
|
|
87
|
+
let out = saveFile(def, 'Text Files|*.txt|All Files|*.*');
|
|
88
|
+
if (!out) {
|
|
89
|
+
const { path } = await inquirer.prompt([
|
|
90
|
+
{
|
|
91
|
+
type: 'input',
|
|
92
|
+
name: 'path',
|
|
93
|
+
message: 'Enter output file path:',
|
|
94
|
+
default: def
|
|
95
|
+
}
|
|
96
|
+
]);
|
|
97
|
+
out = path;
|
|
98
|
+
}
|
|
99
|
+
await outputText(result, { showPreview: false, savePath: out });
|
|
100
|
+
} else if (outputMethod === 'preview') {
|
|
101
|
+
await outputText(result, { showPreview: true, threshold: 500, previewHead: 80, previewTail: 80 });
|
|
102
|
+
} else {
|
|
103
|
+
await outputText(result, { showPreview: false });
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.error('Error processing Base64:', e.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { selectFromMenu } from '../utils/menu.js';
|
|
5
|
+
import { outputText, defaultFileName } from '../utils/output.js';
|
|
6
|
+
import clipboardy from 'clipboardy';
|
|
7
|
+
import { selectFile, saveFile } from '../utils/fileDialog.js';
|
|
8
|
+
|
|
9
|
+
export async function imgBase64Handler() {
|
|
10
|
+
const mode = await selectFromMenu('Image <-> Base64', [
|
|
11
|
+
{ name: 'Image -> Base64', value: 'img2b64' },
|
|
12
|
+
{ name: 'Base64 -> Image', value: 'b642img' }
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
if (mode === 'img2b64') {
|
|
16
|
+
const method = await selectFromMenu('File input method', [
|
|
17
|
+
{ name: 'Select file (dialog)', value: 'dialog' },
|
|
18
|
+
{ name: 'Enter file path manually', value: 'manual' }
|
|
19
|
+
]);
|
|
20
|
+
let filePath = '';
|
|
21
|
+
if (method === 'dialog') {
|
|
22
|
+
filePath = selectFile('Image Files|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.webp|All Files|*.*') || '';
|
|
23
|
+
if (!filePath) {
|
|
24
|
+
console.log('File dialog not available or canceled. Please enter path manually.');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!filePath) {
|
|
28
|
+
const ans = await inquirer.prompt([
|
|
29
|
+
{
|
|
30
|
+
type: 'input',
|
|
31
|
+
name: 'filePath',
|
|
32
|
+
message: 'Enter image file path:',
|
|
33
|
+
validate: (input) => (fs.existsSync(input) ? true : 'File does not exist.')
|
|
34
|
+
}
|
|
35
|
+
]);
|
|
36
|
+
filePath = ans.filePath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const fileData = fs.readFileSync(filePath);
|
|
41
|
+
const base64 = fileData.toString('base64');
|
|
42
|
+
const outputMethod = await selectFromMenu('Output', [
|
|
43
|
+
{ name: 'Copy to clipboard', value: 'copy' },
|
|
44
|
+
{ name: 'Save base64 to file', value: 'file' },
|
|
45
|
+
{ name: 'Preview + copy', value: 'preview' }
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
if (outputMethod === 'file') {
|
|
49
|
+
const def = defaultFileName('image-base64', 'txt');
|
|
50
|
+
let outPath = saveFile(def, 'Text Files|*.txt|All Files|*.*');
|
|
51
|
+
if (!outPath) {
|
|
52
|
+
const ans = await inquirer.prompt([
|
|
53
|
+
{
|
|
54
|
+
type: 'input',
|
|
55
|
+
name: 'path',
|
|
56
|
+
message: 'Enter output file path (e.g. base64.txt):',
|
|
57
|
+
default: def
|
|
58
|
+
}
|
|
59
|
+
]);
|
|
60
|
+
outPath = ans.path;
|
|
61
|
+
}
|
|
62
|
+
await outputText(base64, { showPreview: false, savePath: outPath });
|
|
63
|
+
} else if (outputMethod === 'preview') {
|
|
64
|
+
await outputText(base64, { showPreview: true, threshold: 500, previewHead: 80, previewTail: 80 });
|
|
65
|
+
} else {
|
|
66
|
+
await outputText(base64, { showPreview: false });
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error('Error reading file:', e.message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
} else {
|
|
73
|
+
const src = await selectFromMenu('Input Source', [
|
|
74
|
+
{ name: 'Clipboard', value: 'clip' },
|
|
75
|
+
{ name: 'File', value: 'file' },
|
|
76
|
+
{ name: 'Manual input', value: 'manual' }
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
let content = '';
|
|
80
|
+
if (src === 'clip') {
|
|
81
|
+
try {
|
|
82
|
+
content = await clipboardy.read();
|
|
83
|
+
if (!content) {
|
|
84
|
+
console.log('Clipboard is empty.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error('Failed to read clipboard:', e.message);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
} else if (src === 'file') {
|
|
92
|
+
const method2 = await selectFromMenu('File input method', [
|
|
93
|
+
{ name: 'Select file (dialog)', value: 'dialog' },
|
|
94
|
+
{ name: 'Enter file path manually', value: 'manual' }
|
|
95
|
+
]);
|
|
96
|
+
let inPath = '';
|
|
97
|
+
if (method2 === 'dialog') {
|
|
98
|
+
inPath = selectFile('Text Files|*.txt;*.log;*.md|All Files|*.*') || '';
|
|
99
|
+
if (!inPath) {
|
|
100
|
+
console.log('File dialog not available or canceled. Please enter path manually.');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!inPath) {
|
|
104
|
+
const ans = await inquirer.prompt([
|
|
105
|
+
{
|
|
106
|
+
type: 'input',
|
|
107
|
+
name: 'path',
|
|
108
|
+
message: 'Enter file path containing base64:',
|
|
109
|
+
validate: (p) => (fs.existsSync(p) ? true : 'File does not exist.')
|
|
110
|
+
}
|
|
111
|
+
]);
|
|
112
|
+
inPath = ans.path;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
content = fs.readFileSync(inPath, 'utf-8');
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error('Failed to read file:', e.message);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
const { base64Str } = await inquirer.prompt([
|
|
122
|
+
{
|
|
123
|
+
type: 'input',
|
|
124
|
+
name: 'base64Str',
|
|
125
|
+
message: 'Enter Base64 string:'
|
|
126
|
+
}
|
|
127
|
+
]);
|
|
128
|
+
content = base64Str;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Strip prefix if exists
|
|
132
|
+
const matches = content.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
|
133
|
+
if (matches && matches.length === 3) {
|
|
134
|
+
content = matches[2];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let ext = 'png';
|
|
138
|
+
if (matches && matches[1] && matches[1].startsWith('image/')) {
|
|
139
|
+
const type = matches[1].split('/')[1];
|
|
140
|
+
if (type) ext = type;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let outputPath = saveFile(defaultFileName('image', ext), `Image Files|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.webp|All Files|*.*`);
|
|
144
|
+
if (!outputPath) {
|
|
145
|
+
const ans2 = await inquirer.prompt([
|
|
146
|
+
{
|
|
147
|
+
type: 'input',
|
|
148
|
+
name: 'outputPath',
|
|
149
|
+
message: 'Enter output image path (e.g. output.png):',
|
|
150
|
+
default: defaultFileName('image', ext)
|
|
151
|
+
}
|
|
152
|
+
]);
|
|
153
|
+
outputPath = ans2.outputPath;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const buffer = Buffer.from(content, 'base64');
|
|
158
|
+
fs.writeFileSync(outputPath, buffer);
|
|
159
|
+
console.log(`Image saved to ${outputPath}`);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.error('Error saving image:', e.message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { LoremIpsum } from 'lorem-ipsum';
|
|
3
|
+
import { copy } from '../utils/clipboard.js';
|
|
4
|
+
import { selectFromMenu } from '../utils/menu.js';
|
|
5
|
+
|
|
6
|
+
const lorem = new LoremIpsum({
|
|
7
|
+
sentencesPerParagraph: {
|
|
8
|
+
max: 8,
|
|
9
|
+
min: 4
|
|
10
|
+
},
|
|
11
|
+
wordsPerSentence: {
|
|
12
|
+
max: 16,
|
|
13
|
+
min: 4
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function generateChinese(length) {
|
|
18
|
+
// Common Chinese characters (simplified)
|
|
19
|
+
const commonChars = "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数角路最题验打果指气流接南情场变由规德问展七九几欲问无程和气光料村员真眼体别";
|
|
20
|
+
let result = '';
|
|
21
|
+
for (let i = 0; i < length; i++) {
|
|
22
|
+
const randomIndex = Math.floor(Math.random() * commonChars.length);
|
|
23
|
+
result += commonChars[randomIndex];
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function mockHandler() {
|
|
29
|
+
const type = await selectFromMenu('Mock Text', [
|
|
30
|
+
{ name: 'English', value: 'en' },
|
|
31
|
+
{ name: 'Chinese', value: 'cn' }
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
let result = '';
|
|
35
|
+
|
|
36
|
+
if (type === 'en') {
|
|
37
|
+
const { count } = await inquirer.prompt([
|
|
38
|
+
{
|
|
39
|
+
type: 'number',
|
|
40
|
+
name: 'count',
|
|
41
|
+
message: 'How many paragraphs?',
|
|
42
|
+
default: 1
|
|
43
|
+
}
|
|
44
|
+
]);
|
|
45
|
+
result = lorem.generateParagraphs(count);
|
|
46
|
+
} else {
|
|
47
|
+
const { count } = await inquirer.prompt([
|
|
48
|
+
{
|
|
49
|
+
type: 'number',
|
|
50
|
+
name: 'count',
|
|
51
|
+
message: 'How many characters?',
|
|
52
|
+
default: 50
|
|
53
|
+
}
|
|
54
|
+
]);
|
|
55
|
+
result = generateChinese(count);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`\nResult:\n${result}\n`);
|
|
59
|
+
await copy(result);
|
|
60
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { pinyin } from 'pinyin';
|
|
3
|
+
import { copy } from '../utils/clipboard.js';
|
|
4
|
+
|
|
5
|
+
export async function pinyinHandler() {
|
|
6
|
+
const { input } = await inquirer.prompt([
|
|
7
|
+
{
|
|
8
|
+
type: 'input',
|
|
9
|
+
name: 'input',
|
|
10
|
+
message: 'Enter Chinese text:'
|
|
11
|
+
}
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
// Default style: Normal (no tone)
|
|
15
|
+
const result = pinyin(input, {
|
|
16
|
+
style: pinyin.STYLE_NORMAL, // No tone
|
|
17
|
+
heteronym: false // No heteronym
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Result is an array of arrays. Join them.
|
|
21
|
+
const flatResult = result.flat().join(' ');
|
|
22
|
+
|
|
23
|
+
console.log(`\nPinyin: ${flatResult}\n`);
|
|
24
|
+
await copy(flatResult);
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { copy } from '../utils/clipboard.js';
|
|
4
|
+
|
|
5
|
+
export async function timeFormatHandler() {
|
|
6
|
+
const { input } = await inquirer.prompt([
|
|
7
|
+
{
|
|
8
|
+
type: 'input',
|
|
9
|
+
name: 'input',
|
|
10
|
+
message: 'Enter timestamp or date string (leave empty for current time):'
|
|
11
|
+
}
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
let date;
|
|
15
|
+
if (!input) {
|
|
16
|
+
date = dayjs();
|
|
17
|
+
} else {
|
|
18
|
+
// Check if numeric (timestamp)
|
|
19
|
+
if (/^\d+$/.test(input)) {
|
|
20
|
+
const ts = parseInt(input);
|
|
21
|
+
// Handle seconds vs milliseconds (rough guess)
|
|
22
|
+
// If length is 10, it's likely seconds. 13 is ms.
|
|
23
|
+
if (input.length === 10) {
|
|
24
|
+
date = dayjs.unix(ts);
|
|
25
|
+
} else {
|
|
26
|
+
date = dayjs(ts);
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
date = dayjs(input);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!date.isValid()) {
|
|
34
|
+
console.error('Invalid date format.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const formatted = date.format('YYYY-MM-DD HH:mm:ss');
|
|
39
|
+
console.log(`\nFormatted: ${formatted}\n`);
|
|
40
|
+
await copy(formatted);
|
|
41
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { copy } from '../utils/clipboard.js';
|
|
3
|
+
import { selectFromMenu } from '../utils/menu.js';
|
|
4
|
+
|
|
5
|
+
export async function urlHandler() {
|
|
6
|
+
const mode = await selectFromMenu('URL Encode/Decode', [
|
|
7
|
+
{ name: 'Encode', value: 'encode' },
|
|
8
|
+
{ name: 'Decode', value: 'decode' }
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const { input } = await inquirer.prompt([
|
|
12
|
+
{
|
|
13
|
+
type: 'input',
|
|
14
|
+
name: 'input',
|
|
15
|
+
message: `Enter URL to ${mode}:`
|
|
16
|
+
}
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
let result;
|
|
20
|
+
try {
|
|
21
|
+
if (mode === 'encode') {
|
|
22
|
+
result = encodeURIComponent(input);
|
|
23
|
+
} else {
|
|
24
|
+
result = decodeURIComponent(input);
|
|
25
|
+
}
|
|
26
|
+
console.log(`\nResult:\n${result}\n`);
|
|
27
|
+
await copy(result);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error('Error processing URL:', e.message);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { urlHandler } from './commands/url.js';
|
|
4
|
+
import { base64Handler } from './commands/base64.js';
|
|
5
|
+
import { imgBase64Handler } from './commands/imgBase64.js';
|
|
6
|
+
import { timeFormatHandler } from './commands/timeFormat.js';
|
|
7
|
+
import { timestampHandler } from './commands/timestamp.js';
|
|
8
|
+
import { mockHandler } from './commands/mock.js';
|
|
9
|
+
import { uuidHandler } from './commands/uuid.js';
|
|
10
|
+
import { pinyinHandler } from './commands/pinyin.js';
|
|
11
|
+
|
|
12
|
+
process.on('SIGINT', () => {
|
|
13
|
+
console.log('\nBye!');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
});
|
|
16
|
+
process.on('unhandledRejection', (err) => {
|
|
17
|
+
const msg = (err && err.message) ? err.message : String(err);
|
|
18
|
+
if (msg.includes('SIGINT') || (err && err.name === 'ExitPromptError')) {
|
|
19
|
+
console.log('\nBye!');
|
|
20
|
+
process.exit(0);
|
|
21
|
+
} else {
|
|
22
|
+
console.error('Unhandled error:', err);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const features = [
|
|
28
|
+
{ name: 'URL Encode/Decode', value: 'url' },
|
|
29
|
+
{ name: 'String Encode/Decode (Base64)', value: 'base64' },
|
|
30
|
+
{ name: 'Image <-> Base64', value: 'imgBase64' },
|
|
31
|
+
{ name: 'Time Format', value: 'timeFormat' },
|
|
32
|
+
{ name: 'Get Current Timestamp', value: 'timestamp' },
|
|
33
|
+
{ name: 'Mock Text', value: 'mock' },
|
|
34
|
+
{ name: 'Get UUID', value: 'uuid' },
|
|
35
|
+
{ name: 'Chinese to Pinyin', value: 'pinyin' }
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
program
|
|
40
|
+
.version('1.0.0')
|
|
41
|
+
.description('Developer Assistant CLI')
|
|
42
|
+
.action(async () => {
|
|
43
|
+
await showMenu();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
program.parse(process.argv);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function showMenu() {
|
|
50
|
+
console.log('\n=================================');
|
|
51
|
+
console.log(' xw-devtool-cli Menu');
|
|
52
|
+
console.log('=================================');
|
|
53
|
+
features.forEach((feature, index) => {
|
|
54
|
+
console.log(`${index + 1}. ${feature.name}`);
|
|
55
|
+
});
|
|
56
|
+
console.log('0. Exit');
|
|
57
|
+
console.log('=================================\n');
|
|
58
|
+
|
|
59
|
+
const { choice } = await inquirer.prompt([
|
|
60
|
+
{
|
|
61
|
+
type: 'input',
|
|
62
|
+
name: 'choice',
|
|
63
|
+
message: 'Please enter the feature number (0-8):',
|
|
64
|
+
validate: (input) => {
|
|
65
|
+
const num = parseInt(input);
|
|
66
|
+
if (isNaN(num) || num < 0 || num > features.length) {
|
|
67
|
+
return `Please enter a number between 0 and ${features.length}`;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const index = parseInt(choice);
|
|
75
|
+
|
|
76
|
+
if (index === 0) {
|
|
77
|
+
console.log('Bye!');
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const selectedFeature = features[index - 1];
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await handleAction(selectedFeature.value);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Error:', error.message);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Wait a bit or ask to continue?
|
|
90
|
+
// Usually just looping back is fine.
|
|
91
|
+
await showMenu();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function handleAction(action) {
|
|
95
|
+
switch (action) {
|
|
96
|
+
case 'url':
|
|
97
|
+
await urlHandler();
|
|
98
|
+
break;
|
|
99
|
+
case 'base64':
|
|
100
|
+
await base64Handler();
|
|
101
|
+
break;
|
|
102
|
+
case 'imgBase64':
|
|
103
|
+
await imgBase64Handler();
|
|
104
|
+
break;
|
|
105
|
+
case 'timeFormat':
|
|
106
|
+
await timeFormatHandler();
|
|
107
|
+
break;
|
|
108
|
+
case 'timestamp':
|
|
109
|
+
await timestampHandler();
|
|
110
|
+
break;
|
|
111
|
+
case 'mock':
|
|
112
|
+
await mockHandler();
|
|
113
|
+
break;
|
|
114
|
+
case 'uuid':
|
|
115
|
+
await uuidHandler();
|
|
116
|
+
break;
|
|
117
|
+
case 'pinyin':
|
|
118
|
+
await pinyinHandler();
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
console.log('Feature not implemented yet.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import clipboardy from 'clipboardy';
|
|
2
|
+
|
|
3
|
+
export async function copy(text) {
|
|
4
|
+
try {
|
|
5
|
+
await clipboardy.write(text);
|
|
6
|
+
console.log('Result copied to clipboard!');
|
|
7
|
+
} catch (e) {
|
|
8
|
+
console.error('Failed to copy to clipboard (might not be supported in this environment):', e.message);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
function isWindows() {
|
|
4
|
+
return process.platform === 'win32';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function selectFile(filter = 'All Files|*.*') {
|
|
8
|
+
if (!isWindows()) return null;
|
|
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){ Write-Output $fd.FileName }"`;
|
|
11
|
+
const output = execSync(cmd, { stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
12
|
+
return output || null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function saveFile(defaultName = 'output.txt', filter = 'All Files|*.*') {
|
|
19
|
+
if (!isWindows()) return null;
|
|
20
|
+
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){ Write-Output $sd.FileName }"`;
|
|
22
|
+
const output = execSync(cmd, { stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
23
|
+
return output || null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Display a numbered menu and get user selection.
|
|
5
|
+
* @param {string} title - The title of the menu.
|
|
6
|
+
* @param {Array<{name: string, value: any}>} options - List of options.
|
|
7
|
+
* @returns {Promise<any>} - The selected value.
|
|
8
|
+
*/
|
|
9
|
+
export async function selectFromMenu(title, options) {
|
|
10
|
+
console.log(`\n=== ${title} ===`);
|
|
11
|
+
options.forEach((opt, index) => {
|
|
12
|
+
console.log(`${index + 1}. ${opt.name}`);
|
|
13
|
+
});
|
|
14
|
+
console.log('=================\n');
|
|
15
|
+
|
|
16
|
+
const { choice } = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: 'input',
|
|
19
|
+
name: 'choice',
|
|
20
|
+
message: `Select option (1-${options.length}):`,
|
|
21
|
+
validate: (input) => {
|
|
22
|
+
const num = parseInt(input);
|
|
23
|
+
if (isNaN(num) || num < 1 || num > options.length) {
|
|
24
|
+
return `Please enter a number between 1 and ${options.length}`;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
return options[parseInt(choice) - 1].value;
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { copy } from './clipboard.js';
|
|
3
|
+
|
|
4
|
+
export async function outputText(text, options = {}) {
|
|
5
|
+
const {
|
|
6
|
+
showPreview = true,
|
|
7
|
+
previewHead = 50,
|
|
8
|
+
previewTail = 50,
|
|
9
|
+
threshold = 500,
|
|
10
|
+
savePath
|
|
11
|
+
} = options;
|
|
12
|
+
|
|
13
|
+
if (savePath) {
|
|
14
|
+
fs.writeFileSync(savePath, text, 'utf-8');
|
|
15
|
+
console.log(`Saved to ${savePath}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (showPreview) {
|
|
19
|
+
if (text.length > threshold) {
|
|
20
|
+
console.log(`Length: ${text.length}`);
|
|
21
|
+
const head = text.slice(0, previewHead);
|
|
22
|
+
const tail = text.slice(-previewTail);
|
|
23
|
+
console.log(`${head}...${tail}`);
|
|
24
|
+
} else {
|
|
25
|
+
console.log(`Result:\n${text}\n`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await copy(text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function defaultFileName(tool, ext = 'txt') {
|
|
33
|
+
const ts = Date.now();
|
|
34
|
+
return `${tool}-${ts}.${ext}`;
|
|
35
|
+
}
|