xw-devtool-cli 1.0.1 → 1.0.3

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 CHANGED
@@ -1,73 +1,122 @@
1
1
  # xw-devtool-cli
2
2
 
3
- 一个上手即用的命令行工具箱,帮你快速完成常见开发小任务:URL 编解码、字符串 Base64、图片与 Base64 互转、时间格式化与当前时间戳、Mock 文本(中/英)、UUID、中文转拼音等,且工具持续添加中......,结果会自动复制到剪贴板,方便粘贴到代码或文档。
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
- 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
34
+ 安装后即可在终端直接使用 `xw-devtool` 命令。
35
+
36
+ ### 免安装运行 (npx)
37
+ ```bash
38
+ npx xw-devtool-cli@latest
22
39
  ```
23
40
 
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`
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.1",
3
+ "version": "1.0.3",
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": "git+https://example.com/xw-devtool-cli.git"
42
+ "url": "https://gitee.com/github-9819409/xw-devtool-cli.git"
40
43
  },
41
44
  "bugs": {
42
- "url": "https://example.com/xw-devtool-cli/issues"
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,8 @@
48
51
  "inquirer": "^13.1.0",
49
52
  "lorem-ipsum": "^2.0.8",
50
53
  "pinyin": "^4.0.0",
54
+ "sharp": "^0.33.5",
55
+ "tinycolor2": "^1.6.0",
51
56
  "uuid": "^13.0.0"
52
57
  }
53
58
  }
@@ -0,0 +1,56 @@
1
+ import inquirer from 'inquirer';
2
+ import tinycolor from 'tinycolor2';
3
+ import { copy } from '../utils/clipboard.js';
4
+ import { selectFromMenu } from '../utils/menu.js';
5
+
6
+ export async function colorHandler() {
7
+ const { input } = await inquirer.prompt([
8
+ {
9
+ type: 'input',
10
+ name: 'input',
11
+ message: 'Enter color (Hex, RGB, HSL, or Name):',
12
+ validate: (input) => {
13
+ const color = tinycolor(input);
14
+ return color.isValid() || 'Invalid color format';
15
+ }
16
+ }
17
+ ]);
18
+
19
+ const color = tinycolor(input);
20
+
21
+ const results = [];
22
+ results.push(`Hex: ${color.toHexString().toUpperCase()}`);
23
+ results.push(`RGB: ${color.toRgbString()}`);
24
+ results.push(`HSL: ${color.toHslString()}`);
25
+ results.push(`HSV: ${color.toHsvString()}`);
26
+
27
+ // CMYK conversion (manual, as tinycolor doesn't support it directly)
28
+ const rgb = color.toRgb();
29
+ const r = rgb.r / 255;
30
+ const g = rgb.g / 255;
31
+ const b = rgb.b / 255;
32
+ let k = 1 - Math.max(r, g, b);
33
+ let c = (1 - r - k) / (1 - k) || 0;
34
+ let m = (1 - g - k) / (1 - k) || 0;
35
+ let y = (1 - b - k) / (1 - k) || 0;
36
+
37
+ // Round to 2 decimal places
38
+ const toPercent = (n) => Math.round(n * 100);
39
+ const cmyk = `cmyk(${toPercent(c)}%, ${toPercent(m)}%, ${toPercent(y)}%, ${toPercent(k)}%)`;
40
+ results.push(`CMYK: ${cmyk}`);
41
+
42
+ console.log('\n=== Conversion Results ===');
43
+ results.forEach(res => console.log(res));
44
+ console.log('==========================\n');
45
+
46
+ // Allow user to copy one of the formats
47
+ const copyChoice = await selectFromMenu('Select format to copy', [
48
+ { name: 'Hex', value: color.toHexString().toUpperCase() },
49
+ { name: 'RGB', value: color.toRgbString() },
50
+ { name: 'HSL', value: color.toHslString() },
51
+ { name: 'HSV', value: color.toHsvString() },
52
+ { name: 'CMYK', value: cmyk }
53
+ ]);
54
+
55
+ await copy(copyChoice);
56
+ }
@@ -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
+ }
@@ -1,6 +1,10 @@
1
1
  import inquirer from 'inquirer';
2
2
  import dayjs from 'dayjs';
3
+ import 'dayjs/locale/zh-cn.js';
3
4
  import { copy } from '../utils/clipboard.js';
5
+ import { selectFromMenu } from '../utils/menu.js';
6
+
7
+ dayjs.locale('zh-cn');
4
8
 
5
9
  export async function timeFormatHandler() {
6
10
  const { input } = await inquirer.prompt([
@@ -35,7 +39,26 @@ export async function timeFormatHandler() {
35
39
  return;
36
40
  }
37
41
 
38
- const formatted = date.format('YYYY-MM-DD HH:mm:ss');
42
+ const formatPattern = await selectFromMenu('Select Output Format', [
43
+ { name: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss' },
44
+ { name: 'YYYY-MM-DD HH:mm', value: 'YYYY-MM-DD HH:mm' },
45
+ { name: 'YYYY-MM-DD', value: 'YYYY-MM-DD' },
46
+ { name: 'HH:mm:ss', value: 'HH:mm:ss' },
47
+ { name: 'YYYY-MM-DD HH:mm:ss dddd', value: 'YYYY-MM-DD HH:mm:ss dddd' },
48
+ { name: 'dddd', value: 'dddd' },
49
+ { name: 'Timestamp (ms)', value: 'timestamp-ms' },
50
+ { name: 'Timestamp (s)', value: 'timestamp-s' }
51
+ ]);
52
+
53
+ let formatted;
54
+ if (formatPattern === 'timestamp-ms') {
55
+ formatted = String(date.valueOf());
56
+ } else if (formatPattern === 'timestamp-s') {
57
+ formatted = String(date.unix());
58
+ } else {
59
+ formatted = date.format(formatPattern);
60
+ }
61
+
39
62
  console.log(`\nFormatted: ${formatted}\n`);
40
63
  await copy(formatted);
41
64
  }
@@ -0,0 +1,94 @@
1
+ import inquirer from 'inquirer';
2
+ import { copy, read } from '../utils/clipboard.js';
3
+ import { selectFromMenu } from '../utils/menu.js';
4
+
5
+ export async function variableFormatHandler() {
6
+ const { input } = await inquirer.prompt([
7
+ {
8
+ type: 'input',
9
+ name: 'input',
10
+ message: 'Enter variable name to convert (Press Enter to paste from clipboard):',
11
+ }
12
+ ]);
13
+
14
+ let textToConvert = input;
15
+
16
+ if (!textToConvert || textToConvert.trim().length === 0) {
17
+ textToConvert = await read();
18
+ if (!textToConvert || textToConvert.trim().length === 0) {
19
+ console.log('Clipboard is empty or could not be read.');
20
+ return;
21
+ }
22
+ console.log(`\nUsing clipboard content: "${textToConvert}"`);
23
+ }
24
+
25
+ const words = splitIntoWords(textToConvert);
26
+
27
+ if (words.length === 0) {
28
+ console.log('Could not parse any words from input.');
29
+ return;
30
+ }
31
+
32
+ const results = {
33
+ 'camelCase': toCamelCase(words),
34
+ 'PascalCase': toPascalCase(words),
35
+ 'snake_case': toSnakeCase(words),
36
+ 'kebab-case': toKebabCase(words),
37
+ 'CONSTANT_CASE': toConstantCase(words)
38
+ };
39
+
40
+ console.log('\n=== Conversion Results ===');
41
+ Object.entries(results).forEach(([key, value]) => {
42
+ console.log(`${key.padEnd(15)}: ${value}`);
43
+ });
44
+ console.log('==========================\n');
45
+
46
+ const copyChoice = await selectFromMenu('Select format to copy',
47
+ Object.entries(results).map(([key, value]) => ({
48
+ name: `${key} (${value})`,
49
+ value: value
50
+ }))
51
+ );
52
+
53
+ await copy(copyChoice);
54
+ }
55
+
56
+ function splitIntoWords(str) {
57
+ // 1. Handle CamelCase/camelCase by inserting space before uppercase letters that follow lowercase letters
58
+ let temp = str.replace(/([a-z])([A-Z])/g, '$1 $2');
59
+
60
+ // 2. Handle consecutive uppercase followed by lowercase (e.g. JSONParser -> JSON Parser)
61
+ temp = temp.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
62
+
63
+ // 3. Replace non-alphanumeric characters with spaces
64
+ temp = temp.replace(/[^a-zA-Z0-9]+/g, ' ');
65
+
66
+ // 4. Split by whitespace and filter empty strings
67
+ return temp
68
+ .trim()
69
+ .split(/\s+/)
70
+ .filter(w => w.length > 0);
71
+ }
72
+
73
+ function toCamelCase(words) {
74
+ return words.map((w, i) => {
75
+ if (i === 0) return w.toLowerCase();
76
+ return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
77
+ }).join('');
78
+ }
79
+
80
+ function toPascalCase(words) {
81
+ return words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
82
+ }
83
+
84
+ function toSnakeCase(words) {
85
+ return words.map(w => w.toLowerCase()).join('_');
86
+ }
87
+
88
+ function toKebabCase(words) {
89
+ return words.map(w => w.toLowerCase()).join('-');
90
+ }
91
+
92
+ function toConstantCase(words) {
93
+ return words.map(w => w.toUpperCase()).join('_');
94
+ }
package/src/index.js CHANGED
@@ -3,11 +3,13 @@ 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
- import { timestampHandler } from './commands/timestamp.js';
8
8
  import { mockHandler } from './commands/mock.js';
9
9
  import { uuidHandler } from './commands/uuid.js';
10
10
  import { pinyinHandler } from './commands/pinyin.js';
11
+ import { colorHandler } from './commands/color.js';
12
+ import { variableFormatHandler } from './commands/variableFormat.js';
11
13
 
12
14
  process.on('SIGINT', () => {
13
15
  console.log('\nBye!');
@@ -28,11 +30,13 @@ const features = [
28
30
  { name: 'URL Encode/Decode', value: 'url' },
29
31
  { name: 'String Encode/Decode (Base64)', value: 'base64' },
30
32
  { name: 'Image <-> Base64', value: 'imgBase64' },
31
- { name: 'Time Format', value: 'timeFormat' },
32
- { name: 'Get Current Timestamp', value: 'timestamp' },
33
+ { name: 'Image Format Convert', value: 'imgConvert' },
34
+ { name: 'Time Format / Timestamp', value: 'timeFormat' },
33
35
  { name: 'Mock Text', value: 'mock' },
34
36
  { name: 'Get UUID', value: 'uuid' },
35
- { name: 'Chinese to Pinyin', value: 'pinyin' }
37
+ { name: 'Chinese to Pinyin', value: 'pinyin' },
38
+ { name: 'Color Converter (Hex <-> RGB)', value: 'color' },
39
+ { name: 'Variable Format Converter', value: 'variableFormat' }
36
40
  ];
37
41
 
38
42
  async function main() {
@@ -46,12 +50,29 @@ async function main() {
46
50
  program.parse(process.argv);
47
51
  }
48
52
 
53
+ function getFeatureKey(index) {
54
+ if (index < 9) {
55
+ return String(index + 1);
56
+ }
57
+ return String.fromCharCode('a'.charCodeAt(0) + (index - 9));
58
+ }
59
+
60
+ function getFeatureIndex(key) {
61
+ if (/^[1-9]$/.test(key)) {
62
+ return parseInt(key) - 1;
63
+ }
64
+ if (/^[a-z]$/.test(key.toLowerCase())) {
65
+ return key.toLowerCase().charCodeAt(0) - 'a'.charCodeAt(0) + 9;
66
+ }
67
+ return -1;
68
+ }
69
+
49
70
  async function showMenu() {
50
71
  console.log('\n=================================');
51
72
  console.log(' xw-devtool-cli Menu');
52
73
  console.log('=================================');
53
74
  features.forEach((feature, index) => {
54
- console.log(`${index + 1}. ${feature.name}`);
75
+ console.log(`${getFeatureKey(index)}. ${feature.name}`);
55
76
  });
56
77
  console.log('0. Exit');
57
78
  console.log('=================================\n');
@@ -60,25 +81,26 @@ async function showMenu() {
60
81
  {
61
82
  type: 'input',
62
83
  name: 'choice',
63
- message: 'Please enter the feature number (0-8):',
84
+ message: `Please enter the feature key (0-9, a-z):`,
64
85
  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}`;
86
+ if (input === '0') return true;
87
+
88
+ const index = getFeatureIndex(input);
89
+ if (index >= 0 && index < features.length) {
90
+ return true;
68
91
  }
69
- return true;
92
+ return 'Invalid selection. Please enter a valid menu key.';
70
93
  }
71
94
  }
72
95
  ]);
73
96
 
74
- const index = parseInt(choice);
75
-
76
- if (index === 0) {
97
+ if (choice === '0') {
77
98
  console.log('Bye!');
78
99
  process.exit(0);
79
100
  }
80
101
 
81
- const selectedFeature = features[index - 1];
102
+ const index = getFeatureIndex(choice);
103
+ const selectedFeature = features[index];
82
104
 
83
105
  try {
84
106
  await handleAction(selectedFeature.value);
@@ -102,12 +124,12 @@ async function handleAction(action) {
102
124
  case 'imgBase64':
103
125
  await imgBase64Handler();
104
126
  break;
127
+ case 'imgConvert':
128
+ await imgConvertHandler();
129
+ break;
105
130
  case 'timeFormat':
106
131
  await timeFormatHandler();
107
132
  break;
108
- case 'timestamp':
109
- await timestampHandler();
110
- break;
111
133
  case 'mock':
112
134
  await mockHandler();
113
135
  break;
@@ -117,6 +139,12 @@ async function handleAction(action) {
117
139
  case 'pinyin':
118
140
  await pinyinHandler();
119
141
  break;
142
+ case 'color':
143
+ await colorHandler();
144
+ break;
145
+ case 'variableFormat':
146
+ await variableFormatHandler();
147
+ break;
120
148
  default:
121
149
  console.log('Feature not implemented yet.');
122
150
  }
@@ -8,3 +8,12 @@ export async function copy(text) {
8
8
  console.error('Failed to copy to clipboard (might not be supported in this environment):', e.message);
9
9
  }
10
10
  }
11
+
12
+ export async function read() {
13
+ try {
14
+ return await clipboardy.read();
15
+ } catch (e) {
16
+ console.error('Failed to read from clipboard:', e.message);
17
+ return '';
18
+ }
19
+ }
@@ -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){ Write-Output $fd.FileName }"`;
11
- const output = execSync(cmd, { stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
12
- return output || null;
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){ Write-Output $sd.FileName }"`;
22
- const output = execSync(cmd, { stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
23
- return output || null;
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
-
@@ -1,7 +0,0 @@
1
- import { copy } from '../utils/clipboard.js';
2
-
3
- export async function timestampHandler() {
4
- const now = Date.now();
5
- console.log(`\nCurrent Timestamp: ${now}\n`);
6
- await copy(String(now));
7
- }