xw-devtool-cli 1.0.38 → 1.0.40
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 +28 -11
- package/README_EN.md +13 -4
- package/package.json +5 -2
- package/src/commands/fileTree.js +125 -0
- package/src/commands/jsonFormat.js +143 -65
- package/src/i18n.js +3 -0
- package/src/index.js +5 -0
- package/src/locales/en.js +37 -1
- package/src/locales/zh.js +37 -1
- package/src/utils/output.js +5 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
一个基于 Node.js 的开发者命令行工具箱,旨在提供开箱即用的常用开发工具,帮助开发者快速处理日常任务。
|
|
6
6
|
|
|
7
|
-
主要功能包括:Base64 编解码、图片格式转换、图片与 Base64 互转、Mock 数据生成、时间戳/日期格式化、时间计算、URL 编解码、UUID 生成、汉字转拼音、颜色转换、变量格式转换、哈希计算、二维码生成、特殊符号大全、Markdown 语法工具、VS Code
|
|
7
|
+
主要功能包括:Base64 编解码、图片格式转换、图片与 Base64 互转、Mock 数据生成、时间戳/日期格式化、时间计算、URL 编解码、UUID 生成、汉字转拼音、颜色转换、变量格式转换、哈希计算、二维码生成、特殊符号大全、Markdown 语法工具、VS Code 代码段生成、当前目录树生成等。大部分结果可一键复制到剪贴板,极大提升开发效率。
|
|
8
8
|
|
|
9
9
|
## ✨ 功能特性
|
|
10
10
|
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
- **Markdown 语法工具**:提供常用 Markdown 语法模板 (Headers, Lists, Tables, Code 等),一键复制。
|
|
52
52
|
- **VS Code 代码段生成器**:将代码转换为 VS Code Snippet JSON,自动生成语法速查表注释。
|
|
53
53
|
- **Git 助手**: 标准化生成 Git 分支名 (feature/bugfix/custom) 和 Conventional Commits 提交日志。
|
|
54
|
+
- **当前目录树生成**:生成当前目录的文件树结构,支持指定层级,输出可选复制到剪贴板或导出为 txt 文件。
|
|
54
55
|
- **占位图生成**:自定义尺寸、背景色、文字颜色和格式生成占位图。
|
|
55
56
|
- **便捷操作**:
|
|
56
57
|
- 支持文件选择对话框 (Windows)。
|
|
@@ -132,7 +133,8 @@ s. Emoji 选择器
|
|
|
132
133
|
t. Markdown 片段
|
|
133
134
|
u. VS Code 代码片段生成
|
|
134
135
|
v. Git 助手 (分支/提交模板)
|
|
135
|
-
w.
|
|
136
|
+
w. 当前目录树生成
|
|
137
|
+
x. 设置 / 语言 (Settings)
|
|
136
138
|
0. 退出
|
|
137
139
|
=================================
|
|
138
140
|
```
|
|
@@ -170,6 +172,15 @@ w. 设置 / 语言 (Settings)
|
|
|
170
172
|
- 自动复制输出目录路径。
|
|
171
173
|
- 完成后可选直接打开输出目录。
|
|
172
174
|
|
|
175
|
+
### 当前目录树生成
|
|
176
|
+
- 选择 `w` 进入。
|
|
177
|
+
- 先选择层级模式:
|
|
178
|
+
- **所有层级**:从当前目录递归到最深层。
|
|
179
|
+
- **指定层级**:输入层级数字(`0` 表示仅根目录,`1` 表示包含第一层子项)。
|
|
180
|
+
- 再选择输出方式:
|
|
181
|
+
- **复制到剪贴板**:自动复制结果,便于粘贴到文档或 Issue 中。
|
|
182
|
+
- **输出为 txt 文件**:可选择保存位置,默认文件名带时间戳,避免覆盖旧文件。
|
|
183
|
+
|
|
173
184
|
### 3. 占位图生成 (Placeholder Image)
|
|
174
185
|
- 选择 `3` 进入。
|
|
175
186
|
- **模式 1:本地图片文件 (Local Image File)**
|
|
@@ -234,13 +245,16 @@ w. 设置 / 语言 (Settings)
|
|
|
234
245
|
|
|
235
246
|
### 10. JSON 格式化
|
|
236
247
|
- 选择 `a` 进入。
|
|
237
|
-
-
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
248
|
+
- 支持两种输入来源:
|
|
249
|
+
- **从剪贴板读取**:自动读取当前剪贴板内容。
|
|
250
|
+
- **从本地文件读取**:支持文件对话框选择或手动输入路径。
|
|
251
|
+
- 优先按标准 JSON 解析;若遇到常见非标准写法(如代码块包裹、单引号、未加引号的 key、尾随逗号、HTML 实体),会自动修正常见问题后再格式化。
|
|
252
|
+
- 统一转换为 2 空格缩进的标准 JSON 格式。
|
|
253
|
+
- 支持在各选项列表中返回上一步。
|
|
254
|
+
- 输出支持:
|
|
255
|
+
- **复制到剪贴板**(默认推荐)。
|
|
256
|
+
- **保存到文件**(默认文件名带时间戳,避免覆盖旧文件)。
|
|
257
|
+
- **注意**:为防止长文本刷屏,结果不会在终端完整打印。
|
|
244
258
|
|
|
245
259
|
### 11. 中文转拼音
|
|
246
260
|
- 选择 `b` 进入。
|
|
@@ -508,8 +522,11 @@ v. Git Helper (Branch/Commit Template) w. Settings / Language
|
|
|
508
522
|
|
|
509
523
|
#### 10. JSON Format
|
|
510
524
|
- Select `a`.
|
|
511
|
-
-
|
|
512
|
-
- **
|
|
525
|
+
- Supports two input sources:
|
|
526
|
+
- **Clipboard**: Read content directly from clipboard.
|
|
527
|
+
- **Local file**: Pick by file dialog or enter file path manually.
|
|
528
|
+
- Strictly parses standard JSON and converts to canonical JSON with 2-space indentation.
|
|
529
|
+
- Supports going back to previous step in option lists.
|
|
513
530
|
- **Note**: Results copied to clipboard only (to avoid flooding terminal).
|
|
514
531
|
|
|
515
532
|
#### 11. Chinese to Pinyin
|
package/README_EN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
A Node.js-based developer command-line toolbox designed to provide out-of-the-box common development tools to help developers handle daily tasks quickly.
|
|
6
6
|
|
|
7
|
-
Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, special symbols, Markdown snippets, VS Code snippet generation, etc.
|
|
7
|
+
Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
@@ -45,6 +45,7 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
|
|
|
45
45
|
- **Markdown Snippets**: Common Markdown templates.
|
|
46
46
|
- **VS Code Snippets**: Generate VS Code snippet JSON from code.
|
|
47
47
|
- **Git Helper**: Generate standardized Git branch names (feature/bugfix) and Conventional Commits messages.
|
|
48
|
+
- **Current Directory Tree**: Generate the file tree for the current directory with optional depth limit, then choose to copy to clipboard or export as a `.txt` file.
|
|
48
49
|
- **Convenience**:
|
|
49
50
|
- File selection dialog (Windows).
|
|
50
51
|
- Auto-copy results to clipboard.
|
|
@@ -119,6 +120,7 @@ i. Special Characters (Symbols)
|
|
|
119
120
|
j. Emoji Picker
|
|
120
121
|
k. Markdown Snippets
|
|
121
122
|
l. VS Code Snippet Generator
|
|
123
|
+
m. Current Directory Tree
|
|
122
124
|
s. Settings (Language)
|
|
123
125
|
0. Exit
|
|
124
126
|
=================================
|
|
@@ -187,9 +189,16 @@ s. Settings (Language)
|
|
|
187
189
|
|
|
188
190
|
### 10. JSON Format
|
|
189
191
|
- Select `a`.
|
|
190
|
-
-
|
|
191
|
-
- **
|
|
192
|
-
- **
|
|
192
|
+
- Supports two input sources:
|
|
193
|
+
- **Clipboard**: Read content directly from clipboard.
|
|
194
|
+
- **Local file**: Pick by file dialog or enter file path manually.
|
|
195
|
+
- Parses standard JSON first; if it detects common non-standard patterns (code fences, single quotes, unquoted keys, trailing commas, HTML entities), it auto-fixes and then formats.
|
|
196
|
+
- Converts to canonical JSON with 2-space indentation.
|
|
197
|
+
- Supports going back to previous step in option lists.
|
|
198
|
+
- Output supports:
|
|
199
|
+
- **Copy to clipboard** (recommended).
|
|
200
|
+
- **Save to file** (default filename includes timestamp to avoid overwriting old files).
|
|
201
|
+
- **Note**: Full output is not printed to terminal to avoid flooding.
|
|
193
202
|
|
|
194
203
|
### 11. Chinese to Pinyin
|
|
195
204
|
- Select `b`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xw-devtool-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "基于node的开发者助手cli",
|
|
6
6
|
"main": "index.js",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"placeholder-image",
|
|
44
44
|
"qrcode",
|
|
45
45
|
"json-format",
|
|
46
|
+
"json-formatter",
|
|
46
47
|
"variable-case",
|
|
47
48
|
"hash",
|
|
48
49
|
"encryption",
|
|
@@ -59,7 +60,9 @@
|
|
|
59
60
|
"git",
|
|
60
61
|
"git-branch",
|
|
61
62
|
"commit-message",
|
|
62
|
-
"conventional-commits"
|
|
63
|
+
"conventional-commits",
|
|
64
|
+
"directory-tree",
|
|
65
|
+
"file-tree"
|
|
63
66
|
],
|
|
64
67
|
"author": "npmxw",
|
|
65
68
|
"license": "ISC",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import i18next from '../i18n.js';
|
|
5
|
+
import { selectFromMenu } from '../utils/menu.js';
|
|
6
|
+
import { saveFile } from '../utils/fileDialog.js';
|
|
7
|
+
import { defaultFileName, outputText } from '../utils/output.js';
|
|
8
|
+
|
|
9
|
+
export async function fileTreeHandler() {
|
|
10
|
+
const mode = await selectFromMenu(
|
|
11
|
+
i18next.t('fileTree.depthMode'),
|
|
12
|
+
[
|
|
13
|
+
{ name: i18next.t('fileTree.allDepth'), value: 'all' },
|
|
14
|
+
{ name: i18next.t('fileTree.customDepth'), value: 'custom' }
|
|
15
|
+
],
|
|
16
|
+
true,
|
|
17
|
+
i18next.t('common.back')
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (mode === '__BACK__') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let maxDepth = Infinity;
|
|
25
|
+
if (mode === 'custom') {
|
|
26
|
+
const { depth } = await inquirer.prompt([
|
|
27
|
+
{
|
|
28
|
+
type: 'input',
|
|
29
|
+
name: 'depth',
|
|
30
|
+
message: i18next.t('fileTree.depthPrompt'),
|
|
31
|
+
validate: (input) => {
|
|
32
|
+
const value = Number.parseInt(input, 10);
|
|
33
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
34
|
+
return i18next.t('fileTree.depthInvalid');
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]);
|
|
40
|
+
maxDepth = Number.parseInt(depth, 10);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const outputMode = await selectFromMenu(
|
|
44
|
+
i18next.t('fileTree.outputMode'),
|
|
45
|
+
[
|
|
46
|
+
{ name: i18next.t('fileTree.outputClipboard'), value: 'clipboard' },
|
|
47
|
+
{ name: i18next.t('fileTree.outputFile'), value: 'file' }
|
|
48
|
+
],
|
|
49
|
+
true,
|
|
50
|
+
i18next.t('common.back')
|
|
51
|
+
);
|
|
52
|
+
if (outputMode === '__BACK__') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cwd = process.cwd();
|
|
57
|
+
const tree = buildTree(cwd, maxDepth);
|
|
58
|
+
const depthText = Number.isFinite(maxDepth) ? String(maxDepth) : i18next.t('fileTree.allDepth');
|
|
59
|
+
|
|
60
|
+
if (outputMode === 'file') {
|
|
61
|
+
const defaultName = defaultFileName('file-tree', 'txt');
|
|
62
|
+
let savePath = saveFile(defaultName, 'Text Files|*.txt|All Files|*.*') || '';
|
|
63
|
+
if (!savePath) {
|
|
64
|
+
const answer = await inquirer.prompt([
|
|
65
|
+
{
|
|
66
|
+
type: 'input',
|
|
67
|
+
name: 'path',
|
|
68
|
+
message: i18next.t('fileTree.enterOutputPath'),
|
|
69
|
+
default: defaultName,
|
|
70
|
+
validate: (value) => value.trim() ? true : i18next.t('fileTree.outputPathRequired')
|
|
71
|
+
}
|
|
72
|
+
]);
|
|
73
|
+
savePath = answer.path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await outputText(tree, { showPreview: false, savePath, copyToClipboard: false });
|
|
77
|
+
console.log(i18next.t('fileTree.generatedFile', { path: cwd, depth: depthText, savePath }));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(i18next.t('fileTree.generatedClipboard', { path: cwd, depth: depthText }));
|
|
82
|
+
const preview = tree.length > 2000 ? `${tree.slice(0, 1000)}\n...\n${tree.slice(-800)}` : tree;
|
|
83
|
+
console.log(`\n${preview}\n`);
|
|
84
|
+
await outputText(tree, { showPreview: false });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildTree(rootPath, maxDepth) {
|
|
88
|
+
const rootName = path.basename(rootPath) || rootPath;
|
|
89
|
+
const lines = [rootName];
|
|
90
|
+
|
|
91
|
+
walkDir(rootPath, '', 0, maxDepth, lines);
|
|
92
|
+
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function walkDir(currentPath, prefix, depth, maxDepth, lines) {
|
|
97
|
+
if (depth >= maxDepth) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let entries = [];
|
|
102
|
+
try {
|
|
103
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
lines.push(`${prefix}└── [${i18next.t('fileTree.readFailed')}: ${error.message}]`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
entries.sort((a, b) => {
|
|
110
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
111
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
112
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
entries.forEach((entry, index) => {
|
|
116
|
+
const isLast = index === entries.length - 1;
|
|
117
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
118
|
+
lines.push(`${prefix}${connector}${entry.name}`);
|
|
119
|
+
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
const nextPrefix = `${prefix}${isLast ? ' ' : '│ '}`;
|
|
122
|
+
walkDir(path.join(currentPath, entry.name), nextPrefix, depth + 1, maxDepth, lines);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -1,83 +1,161 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import he from 'he';
|
|
1
3
|
import inquirer from 'inquirer';
|
|
2
|
-
import
|
|
4
|
+
import i18next from '../i18n.js';
|
|
5
|
+
import { read } from '../utils/clipboard.js';
|
|
6
|
+
import { saveFile, selectFile } from '../utils/fileDialog.js';
|
|
3
7
|
import { selectFromMenu } from '../utils/menu.js';
|
|
4
|
-
import
|
|
8
|
+
import { defaultFileName, outputText } from '../utils/output.js';
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
{ name: 'Prettify (4 spaces)', value: 'prettify-4' }
|
|
11
|
-
]);
|
|
10
|
+
function stripCodeFence(text) {
|
|
11
|
+
const match = text.match(/^```(?:json|javascript|js)?\s*([\s\S]*?)\s*```$/i);
|
|
12
|
+
return match ? match[1] : text;
|
|
13
|
+
}
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{ name: 'File', value: 'file' }
|
|
17
|
-
]);
|
|
15
|
+
function normalizeQuotes(text) {
|
|
16
|
+
return text.replace(/[“”]/g, '"').replace(/[‘’]/g, "'");
|
|
17
|
+
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
function relaxJsonLike(text) {
|
|
20
|
+
let result = text;
|
|
21
|
+
result = result.replace(/([{,]\s*)([a-zA-Z_$][\w$-]*)(\s*:)/g, '$1"$2"$3');
|
|
22
|
+
result = result.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, (_, value) => `"${value.replace(/"/g, '\\"')}"`);
|
|
23
|
+
result = result.replace(/,\s*([}\]])/g, '$1');
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseJsonInput(text) {
|
|
28
|
+
const candidates = new Set();
|
|
29
|
+
const trimmed = text.replace(/^\uFEFF/, '').trim();
|
|
30
|
+
const decoded = he.decode(trimmed);
|
|
31
|
+
|
|
32
|
+
[trimmed, decoded].forEach((raw) => {
|
|
33
|
+
if (!raw) return;
|
|
34
|
+
const noFence = stripCodeFence(raw).trim();
|
|
35
|
+
const normalized = normalizeQuotes(noFence);
|
|
36
|
+
const relaxed = relaxJsonLike(normalized);
|
|
37
|
+
candidates.add(noFence);
|
|
38
|
+
candidates.add(normalized);
|
|
39
|
+
candidates.add(relaxed);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let lastError = new Error('Invalid JSON');
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
if (!candidate) continue;
|
|
31
45
|
try {
|
|
32
|
-
|
|
33
|
-
} catch (
|
|
34
|
-
|
|
46
|
+
return JSON.parse(candidate);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
lastError = error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw lastError;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function jsonFormatHandler() {
|
|
55
|
+
const inputSource = await selectFromMenu(
|
|
56
|
+
i18next.t('jsonFormat.selectSource'),
|
|
57
|
+
[
|
|
58
|
+
{ name: i18next.t('jsonFormat.sourceClipboard'), value: 'clipboard' },
|
|
59
|
+
{ name: i18next.t('jsonFormat.sourceFile'), value: 'file' }
|
|
60
|
+
],
|
|
61
|
+
true,
|
|
62
|
+
i18next.t('common.back')
|
|
63
|
+
);
|
|
64
|
+
if (inputSource === '__BACK__') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let inputText = '';
|
|
69
|
+
if (inputSource === 'clipboard') {
|
|
70
|
+
inputText = await read();
|
|
71
|
+
if (!inputText.trim()) {
|
|
72
|
+
console.log(i18next.t('jsonFormat.clipboardEmpty'));
|
|
35
73
|
return;
|
|
36
74
|
}
|
|
37
75
|
} else {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
name: '
|
|
42
|
-
|
|
76
|
+
const inputMethod = await selectFromMenu(
|
|
77
|
+
i18next.t('jsonFormat.fileInputMethod'),
|
|
78
|
+
[
|
|
79
|
+
{ name: i18next.t('jsonFormat.fileInputDialog'), value: 'dialog' },
|
|
80
|
+
{ name: i18next.t('jsonFormat.fileInputManual'), value: 'manual' }
|
|
81
|
+
],
|
|
82
|
+
true,
|
|
83
|
+
i18next.t('common.back')
|
|
84
|
+
);
|
|
85
|
+
if (inputMethod === '__BACK__') {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let filePath = '';
|
|
90
|
+
if (inputMethod === 'dialog') {
|
|
91
|
+
filePath = selectFile('JSON Files|*.json|Text Files|*.txt;*.log|All Files|*.*') || '';
|
|
92
|
+
if (!filePath) {
|
|
93
|
+
console.log(i18next.t('jsonFormat.dialogUnavailable'));
|
|
43
94
|
}
|
|
44
|
-
|
|
45
|
-
|
|
95
|
+
}
|
|
96
|
+
if (!filePath) {
|
|
97
|
+
const answer = await inquirer.prompt([
|
|
98
|
+
{
|
|
99
|
+
type: 'input',
|
|
100
|
+
name: 'path',
|
|
101
|
+
message: i18next.t('jsonFormat.enterFilePath'),
|
|
102
|
+
validate: (value) => (fs.existsSync(value) ? true : i18next.t('jsonFormat.fileNotFound'))
|
|
103
|
+
}
|
|
104
|
+
]);
|
|
105
|
+
filePath = answer.path;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
inputText = fs.readFileSync(filePath, 'utf-8');
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(i18next.t('jsonFormat.readFileError', { message: error.message }));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const normalizedInput = inputText.replace(/^\uFEFF/, '').trim();
|
|
117
|
+
if (!normalizedInput) {
|
|
118
|
+
console.log(i18next.t('jsonFormat.emptyInput'));
|
|
119
|
+
return;
|
|
46
120
|
}
|
|
47
121
|
|
|
48
122
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// This is a very basic attempt to fix unquoted keys
|
|
63
|
-
const fixed = input.replace(/([{,]\s*)([a-zA-Z0-9_]+?)\s*:/g, '$1"$2":');
|
|
64
|
-
jsonObj = JSON.parse(fixed);
|
|
65
|
-
} catch (e2) {
|
|
66
|
-
throw new Error('Invalid JSON format');
|
|
67
|
-
}
|
|
123
|
+
const parsed = parseJsonInput(normalizedInput);
|
|
124
|
+
const formatted = JSON.stringify(parsed, null, 2);
|
|
125
|
+
const outputMode = await selectFromMenu(
|
|
126
|
+
i18next.t('jsonFormat.outputMode'),
|
|
127
|
+
[
|
|
128
|
+
{ name: i18next.t('jsonFormat.outputClipboard'), value: 'clipboard' },
|
|
129
|
+
{ name: i18next.t('jsonFormat.outputFile'), value: 'file' }
|
|
130
|
+
],
|
|
131
|
+
true,
|
|
132
|
+
i18next.t('common.back')
|
|
133
|
+
);
|
|
134
|
+
if (outputMode === '__BACK__') {
|
|
135
|
+
return;
|
|
68
136
|
}
|
|
69
137
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
138
|
+
if (outputMode === 'file') {
|
|
139
|
+
const defaultName = defaultFileName('json-format', 'json');
|
|
140
|
+
let savePath = saveFile(defaultName, 'JSON Files|*.json|Text Files|*.txt|All Files|*.*') || '';
|
|
141
|
+
if (!savePath) {
|
|
142
|
+
const answer = await inquirer.prompt([
|
|
143
|
+
{
|
|
144
|
+
type: 'input',
|
|
145
|
+
name: 'path',
|
|
146
|
+
message: i18next.t('jsonFormat.enterOutputPath'),
|
|
147
|
+
default: defaultName,
|
|
148
|
+
validate: (value) => value.trim() ? true : i18next.t('jsonFormat.outputPathRequired')
|
|
149
|
+
}
|
|
150
|
+
]);
|
|
151
|
+
savePath = answer.path;
|
|
152
|
+
}
|
|
153
|
+
await outputText(formatted, { showPreview: false, savePath });
|
|
154
|
+
return;
|
|
77
155
|
}
|
|
78
|
-
|
|
79
|
-
await
|
|
80
|
-
} catch (
|
|
81
|
-
console.error('
|
|
156
|
+
|
|
157
|
+
await outputText(formatted, { showPreview: false });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error(i18next.t('jsonFormat.invalidJson', { message: error.message }));
|
|
82
160
|
}
|
|
83
161
|
}
|
package/src/i18n.js
CHANGED
package/src/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { pixelDistanceHandler } from './commands/pixelDistance.js';
|
|
|
35
35
|
import { screenMarkHandler } from './commands/screenMark.js';
|
|
36
36
|
import { gitHelperHandler } from './commands/gitHelper.js';
|
|
37
37
|
import { imgCompressHandler } from './commands/imgCompress.js';
|
|
38
|
+
import { fileTreeHandler } from './commands/fileTree.js';
|
|
38
39
|
|
|
39
40
|
process.on('SIGINT', () => {
|
|
40
41
|
console.log(`\n${i18next.t('menu.bye')}`);
|
|
@@ -91,6 +92,7 @@ function getFeatures() {
|
|
|
91
92
|
{ name: i18next.t('menu.features.markdown'), value: 'markdown' },
|
|
92
93
|
{ name: i18next.t('menu.features.vscodeSnippet'), value: 'vscodeSnippet' },
|
|
93
94
|
{ name: i18next.t('menu.features.gitHelper'), value: 'gitHelper' },
|
|
95
|
+
{ name: i18next.t('menu.features.fileTree'), value: 'fileTree' },
|
|
94
96
|
|
|
95
97
|
// Settings
|
|
96
98
|
{ name: i18next.t('menu.features.settings'), value: 'settings' }
|
|
@@ -293,6 +295,9 @@ async function handleAction(action) {
|
|
|
293
295
|
case 'gitHelper':
|
|
294
296
|
await gitHelperHandler();
|
|
295
297
|
break;
|
|
298
|
+
case 'fileTree':
|
|
299
|
+
await fileTreeHandler();
|
|
300
|
+
break;
|
|
296
301
|
case 'settings':
|
|
297
302
|
await handleSettings();
|
|
298
303
|
break;
|
package/src/locales/en.js
CHANGED
|
@@ -50,7 +50,7 @@ export default {
|
|
|
50
50
|
unicode: 'Unicode Encode/Decode',
|
|
51
51
|
htmlEntities: 'HTML Entity Encode/Decode',
|
|
52
52
|
variableFormat: 'Variable Format Converter',
|
|
53
|
-
jsonFormat: 'JSON
|
|
53
|
+
jsonFormat: 'JSON Standard Formatter',
|
|
54
54
|
pinyin: 'Chinese to Pinyin',
|
|
55
55
|
timeFormat: 'Time Format / Timestamp',
|
|
56
56
|
timeCalc: 'Time Calculation (Diff/Offset)',
|
|
@@ -64,6 +64,7 @@ export default {
|
|
|
64
64
|
markdown: 'Markdown Snippets',
|
|
65
65
|
vscodeSnippet: 'VS Code Snippet Generator',
|
|
66
66
|
gitHelper: 'Git Helper (Branch/Commit Template)',
|
|
67
|
+
fileTree: 'Current Directory Tree',
|
|
67
68
|
settings: 'Settings / Language'
|
|
68
69
|
}
|
|
69
70
|
},
|
|
@@ -120,6 +121,26 @@ export default {
|
|
|
120
121
|
error: 'Invalid number for the selected base.',
|
|
121
122
|
copyPrompt: 'Select format to copy'
|
|
122
123
|
},
|
|
124
|
+
jsonFormat: {
|
|
125
|
+
selectSource: 'Select JSON input source',
|
|
126
|
+
sourceClipboard: 'Read from clipboard',
|
|
127
|
+
sourceFile: 'Read from local file',
|
|
128
|
+
fileInputMethod: 'Select file input method',
|
|
129
|
+
fileInputDialog: 'Select file (dialog)',
|
|
130
|
+
fileInputManual: 'Enter file path manually',
|
|
131
|
+
dialogUnavailable: 'File dialog is unavailable or canceled. Please enter the file path manually.',
|
|
132
|
+
enterFilePath: 'Enter JSON file path:',
|
|
133
|
+
fileNotFound: 'File does not exist.',
|
|
134
|
+
readFileError: 'Failed to read file: {{message}}',
|
|
135
|
+
clipboardEmpty: 'Clipboard is empty. Please copy JSON content first.',
|
|
136
|
+
emptyInput: 'Input content is empty.',
|
|
137
|
+
invalidJson: 'Invalid JSON format: {{message}}',
|
|
138
|
+
outputMode: 'Select output method',
|
|
139
|
+
outputClipboard: 'Copy to clipboard',
|
|
140
|
+
outputFile: 'Save to file',
|
|
141
|
+
enterOutputPath: 'Enter output file path:',
|
|
142
|
+
outputPathRequired: 'Output file path cannot be empty.'
|
|
143
|
+
},
|
|
123
144
|
imgSplit: {
|
|
124
145
|
mode: 'Select split mode',
|
|
125
146
|
modeGrid: 'Grid Split (Equal parts)',
|
|
@@ -157,5 +178,20 @@ export default {
|
|
|
157
178
|
scanResult: 'Images found',
|
|
158
179
|
preservePrompt: 'Preserve original directory structure in output?',
|
|
159
180
|
suffixPrompt: 'Append filename suffix (compressed + timestamp)?'
|
|
181
|
+
},
|
|
182
|
+
fileTree: {
|
|
183
|
+
depthMode: 'Select tree depth mode',
|
|
184
|
+
allDepth: 'All levels',
|
|
185
|
+
customDepth: 'Specify depth',
|
|
186
|
+
depthPrompt: 'Enter depth (0 means root only):',
|
|
187
|
+
depthInvalid: 'Please enter an integer greater than or equal to 0',
|
|
188
|
+
outputMode: 'Select output method',
|
|
189
|
+
outputClipboard: 'Copy to clipboard',
|
|
190
|
+
outputFile: 'Export as txt file',
|
|
191
|
+
enterOutputPath: 'Enter output file path:',
|
|
192
|
+
outputPathRequired: 'Output file path is required',
|
|
193
|
+
generatedClipboard: 'Directory tree generated (path: {{path}}, depth: {{depth}}) and copied to clipboard',
|
|
194
|
+
generatedFile: 'Directory tree generated (path: {{path}}, depth: {{depth}}) and saved to file: {{savePath}}',
|
|
195
|
+
readFailed: 'Failed to read directory'
|
|
160
196
|
}
|
|
161
197
|
};
|
package/src/locales/zh.js
CHANGED
|
@@ -50,7 +50,7 @@ export default {
|
|
|
50
50
|
unicode: 'Unicode 编码/解码',
|
|
51
51
|
htmlEntities: 'HTML 实体 编码/解码',
|
|
52
52
|
variableFormat: '变量命名格式转换',
|
|
53
|
-
jsonFormat: 'JSON
|
|
53
|
+
jsonFormat: 'JSON 标准格式化',
|
|
54
54
|
pinyin: '汉字转拼音',
|
|
55
55
|
timeFormat: '时间格式化 / 时间戳',
|
|
56
56
|
timeCalc: '时间计算 (差值/偏移)',
|
|
@@ -64,6 +64,7 @@ export default {
|
|
|
64
64
|
markdown: 'Markdown 片段',
|
|
65
65
|
vscodeSnippet: 'VS Code 代码片段生成',
|
|
66
66
|
gitHelper: 'Git 助手 (分支/提交模板)',
|
|
67
|
+
fileTree: '当前目录树生成',
|
|
67
68
|
settings: '设置 / 语言 (Settings)'
|
|
68
69
|
}
|
|
69
70
|
},
|
|
@@ -120,6 +121,26 @@ export default {
|
|
|
120
121
|
error: '输入的数字对于所选进制无效。',
|
|
121
122
|
copyPrompt: '选择要复制的格式'
|
|
122
123
|
},
|
|
124
|
+
jsonFormat: {
|
|
125
|
+
selectSource: '选择 JSON 输入来源',
|
|
126
|
+
sourceClipboard: '从剪贴板读取',
|
|
127
|
+
sourceFile: '从本地文件读取',
|
|
128
|
+
fileInputMethod: '选择文件读取方式',
|
|
129
|
+
fileInputDialog: '选择文件(对话框)',
|
|
130
|
+
fileInputManual: '手动输入文件路径',
|
|
131
|
+
dialogUnavailable: '文件对话框不可用或已取消,请手动输入文件路径。',
|
|
132
|
+
enterFilePath: '请输入 JSON 文件路径:',
|
|
133
|
+
fileNotFound: '文件不存在。',
|
|
134
|
+
readFileError: '读取文件失败: {{message}}',
|
|
135
|
+
clipboardEmpty: '剪贴板为空,请先复制 JSON 内容。',
|
|
136
|
+
emptyInput: '输入内容为空。',
|
|
137
|
+
invalidJson: 'JSON 格式无效: {{message}}',
|
|
138
|
+
outputMode: '选择输出方式',
|
|
139
|
+
outputClipboard: '复制到剪贴板',
|
|
140
|
+
outputFile: '保存到文件',
|
|
141
|
+
enterOutputPath: '请输入输出文件路径:',
|
|
142
|
+
outputPathRequired: '输出文件路径不能为空。'
|
|
143
|
+
},
|
|
123
144
|
imgSplit: {
|
|
124
145
|
mode: '选择分割模式',
|
|
125
146
|
modeGrid: '网格分割 (等分)',
|
|
@@ -158,5 +179,20 @@ export default {
|
|
|
158
179
|
preservePrompt: '递归时是否按原始目录结构输出'
|
|
159
180
|
,
|
|
160
181
|
suffixPrompt: '是否在文件名添加后缀(compressed+时间戳)'
|
|
182
|
+
},
|
|
183
|
+
fileTree: {
|
|
184
|
+
depthMode: '请选择目录树生成层级',
|
|
185
|
+
allDepth: '所有层级',
|
|
186
|
+
customDepth: '指定层级',
|
|
187
|
+
depthPrompt: '请输入层级(0 表示仅根目录):',
|
|
188
|
+
depthInvalid: '请输入大于等于 0 的整数',
|
|
189
|
+
outputMode: '请选择输出方式',
|
|
190
|
+
outputClipboard: '复制到剪贴板',
|
|
191
|
+
outputFile: '输出为 txt 文件',
|
|
192
|
+
enterOutputPath: '请输入输出文件路径:',
|
|
193
|
+
outputPathRequired: '输出文件路径不能为空',
|
|
194
|
+
generatedClipboard: '目录树已生成(路径: {{path}},层级: {{depth}}),已复制到剪贴板',
|
|
195
|
+
generatedFile: '目录树已生成(路径: {{path}},层级: {{depth}}),已输出到文件: {{savePath}}',
|
|
196
|
+
readFailed: '读取目录失败'
|
|
161
197
|
}
|
|
162
198
|
};
|
package/src/utils/output.js
CHANGED
|
@@ -7,7 +7,8 @@ export async function outputText(text, options = {}) {
|
|
|
7
7
|
previewHead = 50,
|
|
8
8
|
previewTail = 50,
|
|
9
9
|
threshold = 500,
|
|
10
|
-
savePath
|
|
10
|
+
savePath,
|
|
11
|
+
copyToClipboard = true
|
|
11
12
|
} = options;
|
|
12
13
|
|
|
13
14
|
if (savePath) {
|
|
@@ -26,7 +27,9 @@ export async function outputText(text, options = {}) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
if (copyToClipboard) {
|
|
31
|
+
await copy(text);
|
|
32
|
+
}
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export function defaultFileName(tool, ext = 'txt') {
|