@king-3/file-kit 1.0.0-beta.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/bin/cli.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.mjs'
package/dist/cli.mjs ADDED
@@ -0,0 +1,653 @@
1
+ import process from 'node:process';
2
+ import { select as select$2, isCancel, intro } from '@clack/prompts';
3
+ import ansis, { cyan, bold, gray, yellow } from 'ansis';
4
+ import { defineCommand, runMain } from 'citty';
5
+ import path from 'node:path';
6
+ import { fileToBase64, base64ToFile } from '#/core/base64';
7
+ import { tryCatch as tryCatch$1, ValidationError, FileError, CryptoError } from '#/utils/errors';
8
+ import { createCommandContext, buildOutputPath, getPassword, loadArchive } from '#/utils/helpers';
9
+ import { Buffer } from 'node:buffer';
10
+ import { randomBytes, createCipheriv, pbkdf2Sync, createDecipheriv } from 'node:crypto';
11
+ import fs from 'node:fs/promises';
12
+ import { CRYPTO_ALGORITHM } from '#/config/crypto-algorithm';
13
+ import { mkdirSync, accessSync } from 'node:fs';
14
+ import path$1 from 'node:path/posix';
15
+ import { logger as logger$1 } from '#/utils/logger';
16
+ import { AUDIO_FORMATS } from '#/config/audio-formats';
17
+ import { DEFAULT_CONFIG } from '#/config/defaults';
18
+ import { extractAudio } from '#/core/extract';
19
+ import { select as select$1 } from '#/utils/prompts';
20
+
21
+ const base64 = defineCommand({
22
+ meta: {
23
+ name: "base64",
24
+ description: "\u5C06\u6587\u4EF6\u8F6C\u6362\u4E3A Base64 JSON"
25
+ },
26
+ args: {
27
+ input: {
28
+ type: "positional",
29
+ description: "\u8F93\u5165\u6587\u4EF6\u8DEF\u5F84"
30
+ },
31
+ output: {
32
+ type: "string",
33
+ alias: "o",
34
+ description: "\u8F93\u51FA\u76EE\u5F55"
35
+ }
36
+ },
37
+ async run({ args, rawArgs }) {
38
+ const typedArgs = args;
39
+ const ctx = createCommandContext(rawArgs);
40
+ ctx.showIntro();
41
+ tryCatch$1(async () => {
42
+ const inputPath = await ctx.getInput(typedArgs.input, {
43
+ message: "\u8BF7\u8F93\u5165\u6587\u4EF6\u8DEF\u5F84",
44
+ placeholder: "file.txt"
45
+ });
46
+ const outputDir = await ctx.getOutput(typedArgs.output, {
47
+ defaultDir: path.dirname(inputPath)
48
+ });
49
+ const outputPath = buildOutputPath(inputPath, outputDir, "base64.json");
50
+ const loading = ctx.loading("\u6B63\u5728\u8F6C\u6362");
51
+ const archiveData = await fileToBase64(inputPath, outputPath);
52
+ loading.close(
53
+ `\u6587\u4EF6\u5DF2\u4FDD\u5B58\u5230: ${cyan(outputPath)}, \u5171\u8BA1 ${bold.gray((archiveData.file.size / 1024).toFixed(2))} KB`
54
+ );
55
+ ctx.showOutro("\u{1F4E6} \u8F6C\u6362\u5B8C\u6210");
56
+ });
57
+ }
58
+ });
59
+
60
+ function fileExists(targetPath) {
61
+ try {
62
+ accessSync(targetPath);
63
+ return true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ function ensureDir(targetPath) {
69
+ const normalizedPath = path.normalize(targetPath);
70
+ const dirPath = path.extname(normalizedPath) ? path.dirname(normalizedPath) : normalizedPath;
71
+ if (!dirPath || dirPath === "." || dirPath === path.sep) {
72
+ return;
73
+ }
74
+ if (!fileExists(dirPath)) {
75
+ mkdirSync(dirPath, { recursive: true });
76
+ }
77
+ }
78
+ function getFileExt(filePath, options) {
79
+ const ext = path.extname(filePath).toLowerCase();
80
+ return options?.withDot === false ? ext.slice(1) : ext;
81
+ }
82
+ function getFileName(filePath, options) {
83
+ const base = path.basename(filePath);
84
+ return options?.withoutExt ? base.replace(path.extname(base), "") : base;
85
+ }
86
+ const bufferToBase64 = (data) => data.toString("base64");
87
+ const base64ToBuffer = (data) => Buffer.from(data, "base64");
88
+
89
+ function nowUTC8() {
90
+ const now = /* @__PURE__ */ new Date();
91
+ const utc8Time = new Date(now.getTime() + 8 * 60 * 60 * 1e3);
92
+ return utc8Time.toISOString().replace("T", " ").substring(0, 19);
93
+ }
94
+
95
+ const deriveKey = (password, salt) => {
96
+ return pbkdf2Sync(password, salt, 1e5, 32, "sha256");
97
+ };
98
+ async function encrypt$1(filePath, outputPath, options) {
99
+ if (!filePath) {
100
+ throw new ValidationError("\u6587\u4EF6\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A", "filePath");
101
+ }
102
+ if (!outputPath) {
103
+ throw new ValidationError("\u8F93\u51FA\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A", "outputPath");
104
+ }
105
+ try {
106
+ ensureDir(outputPath);
107
+ const fileBuffer = await fs.readFile(filePath);
108
+ const fileName = getFileName(filePath);
109
+ const fileExt = getFileExt(filePath);
110
+ const salt = randomBytes(CRYPTO_ALGORITHM.saltLength);
111
+ const iv = randomBytes(CRYPTO_ALGORITHM.ivLength);
112
+ const key = deriveKey(options.password, salt);
113
+ const cipher = createCipheriv(CRYPTO_ALGORITHM.name, key, iv);
114
+ const encrypted = Buffer.concat([cipher.update(fileBuffer), cipher.final()]);
115
+ const authTag = cipher.getAuthTag();
116
+ const encryptedData = {
117
+ type: "crypto",
118
+ algorithm: CRYPTO_ALGORITHM.name,
119
+ createdAt: nowUTC8(),
120
+ file: {
121
+ name: fileName,
122
+ extension: fileExt,
123
+ size: fileBuffer.length,
124
+ iv: bufferToBase64(iv),
125
+ authTag: bufferToBase64(authTag),
126
+ salt: bufferToBase64(salt),
127
+ encrypted: bufferToBase64(encrypted)
128
+ }
129
+ };
130
+ const content = JSON.stringify(encryptedData, null, 2);
131
+ await fs.writeFile(outputPath, content, "utf-8");
132
+ return encryptedData;
133
+ } catch (error) {
134
+ if (error instanceof ValidationError) {
135
+ throw error;
136
+ }
137
+ if (error.code === "ENOENT") {
138
+ throw new FileError(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`, filePath);
139
+ }
140
+ if (error.code === "EACCES") {
141
+ throw new FileError(`\u6CA1\u6709\u6587\u4EF6\u8BBF\u95EE\u6743\u9650: ${filePath}`, filePath);
142
+ }
143
+ throw new CryptoError(`\u52A0\u5BC6\u5931\u8D25: ${error.message}`, "encrypt");
144
+ }
145
+ }
146
+ async function decrypt$1(archiveData, outputDir = ".", options) {
147
+ if (!archiveData) {
148
+ throw new ValidationError("\u5F52\u6863\u6570\u636E\u4E0D\u80FD\u4E3A\u7A7A", "archiveData");
149
+ }
150
+ if (!archiveData.file?.encrypted) {
151
+ throw new ValidationError("\u5F52\u6863\u6570\u636E\u683C\u5F0F\u9519\u8BEF", "archiveData.file.encrypted");
152
+ }
153
+ try {
154
+ ensureDir(outputDir);
155
+ const { file } = archiveData;
156
+ const salt = base64ToBuffer(file.salt);
157
+ const iv = base64ToBuffer(file.iv);
158
+ const authTag = base64ToBuffer(file.authTag);
159
+ const encrypted = base64ToBuffer(file.encrypted);
160
+ const key = deriveKey(options.password, salt);
161
+ const decipher = createDecipheriv(CRYPTO_ALGORITHM.name, key, iv);
162
+ decipher.setAuthTag(authTag);
163
+ let decrypted;
164
+ try {
165
+ decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
166
+ } catch {
167
+ throw new Error("\u89E3\u5BC6\u5931\u8D25\uFF1A\u5BC6\u7801\u9519\u8BEF\u6216\u6587\u4EF6\u5DF2\u635F\u574F");
168
+ }
169
+ const outputPath = path.join(outputDir, file.name);
170
+ await fs.writeFile(outputPath, decrypted);
171
+ return outputPath;
172
+ } catch (error) {
173
+ if (error instanceof ValidationError) {
174
+ throw error;
175
+ }
176
+ if (error.code === "EACCES") {
177
+ throw new FileError(`\u6CA1\u6709\u76EE\u5F55\u5199\u5165\u6743\u9650: ${outputDir}`, outputDir);
178
+ }
179
+ throw new CryptoError(`\u89E3\u5BC6\u5931\u8D25: ${error.message}`, "decrypt");
180
+ }
181
+ }
182
+
183
+ const logger = {
184
+ success: (msg) => console.log(`${ansis.green("\u2713")} ${msg}`),
185
+ error: (msg) => console.log(`${ansis.red("\u2717")} ${msg}`),
186
+ info: (msg) => console.log(`${ansis.cyan("\u2139")} ${msg}`),
187
+ warn: (msg) => console.log(`${ansis.yellow("\u26A0")} ${msg}`),
188
+ dim: (msg) => console.log(`${ansis.dim("\u2022")} ${msg}`)
189
+ };
190
+
191
+ class AppError extends Error {
192
+ constructor(message, code, details) {
193
+ super(message);
194
+ this.code = code;
195
+ this.details = details;
196
+ this.name = "AppError";
197
+ Error.captureStackTrace(this, this.constructor);
198
+ }
199
+ }
200
+ async function tryCatch(operation) {
201
+ try {
202
+ return await operation();
203
+ } catch (error) {
204
+ handleError(error);
205
+ }
206
+ }
207
+ function handleError(error) {
208
+ if (error instanceof AppError) {
209
+ logger.error(`[${error.code}] ${error.message}`);
210
+ if (error.details && Object.keys(error.details).length > 0) {
211
+ logger.dim(`\u8BE6\u60C5: ${JSON.stringify(error.details, null, 2)}`);
212
+ }
213
+ } else if (error instanceof Error) {
214
+ logger.error(error.message);
215
+ if (process.env.DEBUG) {
216
+ logger.dim(error.stack || "");
217
+ }
218
+ } else {
219
+ logger.error(`\u672A\u77E5\u9519\u8BEF: ${String(error)}`);
220
+ }
221
+ process.exit(1);
222
+ }
223
+
224
+ const decrypt = defineCommand({
225
+ meta: {
226
+ name: "decrypt",
227
+ description: "\u89E3\u5BC6\u6587\u4EF6"
228
+ },
229
+ args: {
230
+ input: {
231
+ type: "positional",
232
+ description: "\u52A0\u5BC6\u6587\u4EF6 (*.crypto.json)",
233
+ required: true
234
+ },
235
+ output: {
236
+ type: "string",
237
+ alias: "o",
238
+ description: "\u8F93\u51FA\u76EE\u5F55"
239
+ },
240
+ password: {
241
+ type: "string",
242
+ alias: "p",
243
+ description: "\u89E3\u5BC6\u5BC6\u7801"
244
+ }
245
+ },
246
+ async run({ args, rawArgs }) {
247
+ const typedArgs = args;
248
+ const ctx = createCommandContext(rawArgs);
249
+ ctx.showIntro();
250
+ tryCatch(async () => {
251
+ const inputPath = await ctx.getInput(typedArgs.input, {
252
+ message: "\u8BF7\u8F93\u5165\u6587\u4EF6\u8DEF\u5F84",
253
+ placeholder: "*.crypto.json",
254
+ validateExtension: ".crypto.json"
255
+ });
256
+ const outputDir = await ctx.getOutput(typedArgs.output, {
257
+ defaultDir: path.dirname(inputPath)
258
+ });
259
+ const password = await getPassword(typedArgs.password);
260
+ const loading = ctx.loading("\u6B63\u5728\u89E3\u5BC6");
261
+ const archiveData = await loadArchive(inputPath, "crypto");
262
+ const outputPath = await decrypt$1(archiveData, outputDir, {
263
+ password
264
+ });
265
+ loading.close(`\u6587\u4EF6\u5DF2\u89E3\u5BC6\u5230: ${cyan(outputPath)}`);
266
+ ctx.showOutro("\u{1F513} \u89E3\u5BC6\u5B8C\u6210");
267
+ });
268
+ }
269
+ });
270
+
271
+ const encrypt = defineCommand({
272
+ meta: {
273
+ name: "encrypt",
274
+ description: "\u52A0\u5BC6\u6587\u4EF6"
275
+ },
276
+ args: {
277
+ input: {
278
+ type: "positional",
279
+ description: "\u6587\u4EF6\u8DEF\u5F84",
280
+ required: true
281
+ },
282
+ output: {
283
+ type: "string",
284
+ alias: "o",
285
+ description: "\u8F93\u51FA\u76EE\u5F55"
286
+ },
287
+ password: {
288
+ type: "string",
289
+ alias: "p",
290
+ description: "\u52A0\u5BC6\u5BC6\u94A5"
291
+ }
292
+ },
293
+ async run({ args, rawArgs }) {
294
+ const typedArgs = args;
295
+ const ctx = createCommandContext(rawArgs);
296
+ ctx.showIntro();
297
+ tryCatch(async () => {
298
+ const inputPath = await ctx.getInput(typedArgs.input, {
299
+ message: "\u8BF7\u8F93\u5165\u6587\u4EF6\u8DEF\u5F84",
300
+ placeholder: "file.txt"
301
+ });
302
+ const outputDir = await ctx.getOutput(typedArgs.output, {
303
+ defaultDir: path$1.dirname(inputPath)
304
+ });
305
+ const password = await getPassword(typedArgs.password);
306
+ console.log(gray("\u2502"));
307
+ logger$1.warn(yellow("\u8BF7\u59A5\u5584\u4FDD\u7BA1\u5BC6\u7801\uFF0C\u4E22\u5931\u540E\u65E0\u6CD5\u6062\u590D\u6587\u4EF6\uFF01"));
308
+ const outputPath = buildOutputPath(inputPath, outputDir, "crypto.json");
309
+ const loading = ctx.loading("\u6B63\u5728\u52A0\u5BC6");
310
+ const archiveData = await encrypt$1(inputPath, outputPath, {
311
+ password
312
+ });
313
+ loading.close(
314
+ `\u6587\u4EF6\u5DF2\u52A0\u5BC6\u5230: ${cyan(outputPath)}, \u5171\u8BA1 ${bold.gray((archiveData.file.size / 1024).toFixed(2))} KB`
315
+ );
316
+ ctx.showOutro("\u{1F510} \u52A0\u5BC6\u5B8C\u6210");
317
+ });
318
+ }
319
+ });
320
+
321
+ const restore = defineCommand({
322
+ meta: {
323
+ name: "restore",
324
+ description: "\u4ECE Base64 JSON \u6062\u590D\u6587\u4EF6"
325
+ },
326
+ args: {
327
+ input: {
328
+ type: "positional",
329
+ description: "Base64 \u6587\u4EF6 (*.base64.json)"
330
+ },
331
+ output: {
332
+ type: "string",
333
+ alias: "o",
334
+ description: "\u8F93\u51FA\u76EE\u5F55"
335
+ }
336
+ },
337
+ async run({ args, rawArgs }) {
338
+ const typedArgs = args;
339
+ const ctx = createCommandContext(rawArgs);
340
+ ctx.showIntro();
341
+ tryCatch$1(async () => {
342
+ const inputPath = await ctx.getInput(typedArgs.input, {
343
+ message: "\u8BF7\u8F93\u5165\u6587\u4EF6\u8DEF\u5F84",
344
+ placeholder: "*.base64.json",
345
+ validateExtension: "base64.json"
346
+ });
347
+ const outputDir = await ctx.getOutput(typedArgs.output, {
348
+ defaultDir: path.dirname(inputPath)
349
+ });
350
+ const loading = ctx.loading("\u6B63\u5728\u6062\u590D");
351
+ const archiveData = await loadArchive(inputPath, "base64");
352
+ const restoredPath = await base64ToFile(archiveData, outputDir);
353
+ loading.close(
354
+ `\u6587\u4EF6\u5DF2\u6062\u590D\u5230: ${cyan(restoredPath)}, \u539F\u59CB\u521B\u5EFA\u65F6\u95F4 ${bold.gray(archiveData.createdAt)}`
355
+ );
356
+ ctx.showOutro("\u{1F504} \u6062\u590D\u5B8C\u6210");
357
+ });
358
+ }
359
+ });
360
+
361
+ const FORMAT_OPTIONS = [
362
+ { value: "mp3", label: "MP3 (\u901A\u7528)", hint: "128k-320k" },
363
+ { value: "aac", label: "AAC/M4A (Apple)", hint: "128k-256k" },
364
+ { value: "flac", label: "FLAC (\u65E0\u635F)", hint: "\u53EF\u538B\u7F29" },
365
+ { value: "alac", label: "ALAC (Apple \u65E0\u635F)", hint: "iTunes/iOS" },
366
+ { value: "wav", label: "WAV (\u65E0\u635F)", hint: "\u6700\u5927" }
367
+ ];
368
+ const getQualityOptions = (format) => {
369
+ if (format === "flac") {
370
+ return [
371
+ { value: "low", label: "\u5FEB\u901F", hint: "\u7EA7\u522B 0" },
372
+ { value: "medium", label: "\u5E73\u8861 (\u63A8\u8350)", hint: "\u7EA7\u522B 5" },
373
+ { value: "high", label: "\u6700\u5927\u538B\u7F29", hint: "\u7EA7\u522B 8" }
374
+ ];
375
+ }
376
+ const formatConfig = AUDIO_FORMATS[format];
377
+ return [
378
+ { value: "low", label: "\u4F4E", hint: formatConfig.quality.low },
379
+ { value: "medium", label: "\u4E2D (\u63A8\u8350)", hint: formatConfig.quality.medium },
380
+ { value: "high", label: "\u9AD8", hint: formatConfig.quality.high }
381
+ ];
382
+ };
383
+ const v2a = defineCommand({
384
+ meta: {
385
+ name: "video-to-audio",
386
+ description: "\u4ECE\u89C6\u9891\u4E2D\u63D0\u53D6\u97F3\u9891"
387
+ },
388
+ args: {
389
+ input: {
390
+ type: "positional",
391
+ description: "\u89C6\u9891\u6587\u4EF6\u8DEF\u5F84"
392
+ },
393
+ output: {
394
+ type: "string",
395
+ alias: "o",
396
+ description: "\u8F93\u51FA\u76EE\u5F55"
397
+ },
398
+ format: {
399
+ type: "string",
400
+ alias: "f",
401
+ description: "\u97F3\u9891\u683C\u5F0F (mp3, aac, flac, alac, wav)"
402
+ },
403
+ quality: {
404
+ type: "string",
405
+ alias: "q",
406
+ description: "\u97F3\u9891\u8D28\u91CF (low, medium, high)"
407
+ }
408
+ },
409
+ async run({ args, rawArgs }) {
410
+ const typedArgs = args;
411
+ const ctx = createCommandContext(rawArgs);
412
+ ctx.showIntro();
413
+ tryCatch$1(async () => {
414
+ const inputPath = await ctx.getInput(typedArgs.input, {
415
+ message: "\u8BF7\u8F93\u5165\u89C6\u9891\u6587\u4EF6\u8DEF\u5F84",
416
+ placeholder: "video.mp4"
417
+ });
418
+ const outputDir = await ctx.getOutput(typedArgs.output, {
419
+ defaultDir: path.dirname(inputPath)
420
+ });
421
+ let format = typedArgs.format;
422
+ if (!format) {
423
+ format = await select$1({
424
+ message: "\u9009\u62E9\u97F3\u9891\u683C\u5F0F",
425
+ options: FORMAT_OPTIONS
426
+ });
427
+ } else if (!AUDIO_FORMATS[format]) {
428
+ logger$1.error(`\u4E0D\u652F\u6301\u7684\u683C\u5F0F: ${format}`);
429
+ logger$1.info(`\u652F\u6301\u7684\u683C\u5F0F: ${Object.keys(AUDIO_FORMATS).join(", ")}`);
430
+ process.exit(1);
431
+ }
432
+ const formatConfig = AUDIO_FORMATS[format];
433
+ let quality;
434
+ if (!typedArgs.quality && formatConfig.needsQuality) {
435
+ const message = format === "flac" ? "\u9009\u62E9\u538B\u7F29\u7EA7\u522B" : "\u9009\u62E9\u97F3\u9891\u8D28\u91CF";
436
+ quality = await select$1({
437
+ message,
438
+ options: getQualityOptions(format)
439
+ });
440
+ } else if (typedArgs.quality) {
441
+ quality = typedArgs.quality;
442
+ } else {
443
+ quality = DEFAULT_CONFIG.videoToAudio.defaultQuality;
444
+ }
445
+ const outputPath = buildOutputPath(
446
+ inputPath,
447
+ outputDir,
448
+ formatConfig.extension
449
+ );
450
+ const loading = ctx.loading("\u6B63\u5728\u63D0\u53D6\u97F3\u9891 0%");
451
+ await extractAudio(
452
+ inputPath,
453
+ outputPath,
454
+ { format, quality },
455
+ (percent) => {
456
+ loading.update(`\u6B63\u5728\u63D0\u53D6\u97F3\u9891 ${percent}%`);
457
+ }
458
+ );
459
+ loading.close(`\u97F3\u9891\u5DF2\u63D0\u53D6\u5230: ${cyan(outputPath)}`);
460
+ ctx.showOutro("\u{1F3B5} \u63D0\u53D6\u5B8C\u6210");
461
+ });
462
+ }
463
+ });
464
+
465
+ const CLI_NAME = "File Kit";
466
+ const CLI_VERSION = "1.0.0";
467
+ const CLI_ALIAS = "fkt";
468
+
469
+ function handleCancel(result) {
470
+ if (isCancel(result)) {
471
+ logger.error(ansis.red("\u64CD\u4F5C\u5DF2\u53D6\u6D88"));
472
+ process.exit(0);
473
+ }
474
+ }
475
+ async function select(options) {
476
+ const result = await select$2(options);
477
+ handleCancel(result);
478
+ return result;
479
+ }
480
+
481
+ const COMMAND_MAP = {
482
+ base64,
483
+ restore,
484
+ "video-to-audio": v2a,
485
+ encrypt,
486
+ decrypt
487
+ };
488
+ const INTERACTIVE_OPTIONS = [
489
+ {
490
+ value: "base64",
491
+ label: ansis.cyan("\u{1F4E6} \u6587\u4EF6\u8F6C Base64"),
492
+ hint: "\u5C06\u4EFB\u610F\u6587\u4EF6\u7F16\u7801\u4E3A Base64 JSON"
493
+ },
494
+ {
495
+ value: "restore",
496
+ label: ansis.green("\u{1F504} Base64 \u8FD8\u539F\u6587\u4EF6"),
497
+ hint: "\u4ECE Base64 JSON \u6062\u590D\u539F\u59CB\u6587\u4EF6"
498
+ },
499
+ {
500
+ value: "video-to-audio",
501
+ label: ansis.magenta("\u{1F3B5} \u89C6\u9891\u63D0\u53D6\u97F3\u9891"),
502
+ hint: "\u4ECE\u89C6\u9891\u4E2D\u63D0\u53D6\u97F3\u9891\u8F68\u9053"
503
+ },
504
+ {
505
+ value: "encrypt",
506
+ label: ansis.red("\u{1F510} \u6587\u4EF6\u52A0\u5BC6"),
507
+ hint: "\u52A0\u5BC6\u6587\u4EF6\u5E76\u751F\u6210 Crypto JSON"
508
+ },
509
+ {
510
+ value: "decrypt",
511
+ label: ansis.green("\u{1F513} \u6587\u4EF6\u89E3\u5BC6"),
512
+ hint: "\u4ECE Crypto JSON \u89E3\u5BC6\u8FD8\u539F\u6587\u4EF6"
513
+ }
514
+ ];
515
+ const cliArgs = {
516
+ help: false,
517
+ version: false
518
+ };
519
+ function preprocessArgs(rawArgs) {
520
+ cliArgs.help = rawArgs.some((arg) => arg === "--help" || arg === "-h");
521
+ cliArgs.version = rawArgs.some((arg) => arg === "--version" || arg === "-v");
522
+ if (cliArgs.help) {
523
+ const excludeHelpArgs = rawArgs.filter(
524
+ (arg) => arg !== "--help" && arg !== "-h"
525
+ );
526
+ process.argv = excludeHelpArgs;
527
+ }
528
+ }
529
+ function showVersion() {
530
+ console.log(
531
+ ansis.cyan(`
532
+ \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E
533
+ \u2502 \u{1F527} ${ansis.bold(CLI_NAME)} \xB7 ${ansis.dim(`v${CLI_VERSION}`.padEnd(9))}\u2502
534
+ \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F
535
+ `)
536
+ );
537
+ console.log(ansis.bold(" \u591A\u529F\u80FD\u6587\u4EF6\u5DE5\u5177\u7BB1\n"));
538
+ console.log(ansis.gray(" \u{1F504} Base64 \u4E92\u8F6C \u{1F3A7} \u97F3\u9891\u63D0\u53D6 \u{1F510} \u6587\u4EF6\u52A0\u5BC6\n"));
539
+ }
540
+ function showHelp() {
541
+ console.log(
542
+ `${ansis.bold(`\u{1F527} ${CLI_NAME}`)}${ansis.dim(` - \u591A\u529F\u80FD\u6587\u4EF6\u5DE5\u5177\u7BB1 (${CLI_ALIAS} v${CLI_VERSION})`)}
543
+ `
544
+ );
545
+ console.log(ansis.bold("\u7528\u6CD5:"));
546
+ console.log(
547
+ ` ${ansis.yellow(CLI_ALIAS)} ${ansis.dim("<command> [options]")} \u6267\u884C\u6307\u5B9A\u547D\u4EE4`
548
+ );
549
+ console.log(
550
+ ` ${ansis.yellow(CLI_ALIAS)} ${ansis.cyan("-i, --interactive")} \u8FDB\u5165\u4EA4\u4E92\u6A21\u5F0F`
551
+ );
552
+ console.log(
553
+ ` ${ansis.yellow(CLI_ALIAS)} ${ansis.cyan("-v, --version")} \u663E\u793A\u7248\u672C\u4FE1\u606F`
554
+ );
555
+ console.log(
556
+ ` ${ansis.yellow(CLI_ALIAS)} ${ansis.cyan("-h, --help")} \u663E\u793A\u5E2E\u52A9\u4FE1\u606F${ansis.dim("\uFF08\u9ED8\u8BA4\uFF09")}
557
+ `
558
+ );
559
+ console.log(ansis.bold("\u547D\u4EE4:"));
560
+ console.log(
561
+ ` ${ansis.cyan("base64")}${ansis.dim(" \u6587\u4EF6\u8F6C Base64")}`
562
+ );
563
+ console.log(
564
+ ` ${ansis.green("restore")}${ansis.dim(" Base64 \u8FD8\u539F\u6587\u4EF6")}`
565
+ );
566
+ console.log(
567
+ ` ${ansis.magenta("video-to-audio, v2a")}${ansis.dim(" \u89C6\u9891\u63D0\u53D6\u97F3\u9891")}`
568
+ );
569
+ console.log(
570
+ ` ${ansis.red("encrypt")}${ansis.dim(" \u52A0\u5BC6\u6587\u4EF6")}`
571
+ );
572
+ console.log(
573
+ ` ${ansis.green("decrypt")}${ansis.dim(" \u89E3\u5BC6\u6587\u4EF6")}
574
+ `
575
+ );
576
+ console.log(ansis.bold("\u793A\u4F8B:"));
577
+ console.log(
578
+ ` ${ansis.yellow(CLI_ALIAS)} ${ansis.cyan("base64")} ${ansis.green("file.txt")} ${ansis.dim("\u8F6C\u6362\u6587\u4EF6\u4E3A Base64")}`
579
+ );
580
+ console.log(
581
+ ` ${ansis.yellow(CLI_ALIAS)} ${ansis.cyan("restore")} ${ansis.green("file.json")} ${ansis.dim("\u8FD8\u539F Base64 \u6587\u4EF6")}`
582
+ );
583
+ console.log(
584
+ ` ${ansis.yellow(CLI_ALIAS)} ${ansis.cyan("v2a")} ${ansis.green("video.mp4")} ${ansis.blue("-f mp3")} ${ansis.dim("\u63D0\u53D6\u89C6\u9891\u97F3\u9891\u4E3A MP3")}`
585
+ );
586
+ console.log(
587
+ ` ${ansis.yellow(CLI_ALIAS)} ${ansis.cyan("encrypt")} ${ansis.green("secret.txt")} ${ansis.blue("-p pwd")} ${ansis.dim("\u52A0\u5BC6\u6587\u4EF6")}`
588
+ );
589
+ console.log(
590
+ ` ${ansis.yellow(CLI_ALIAS)} ${ansis.cyan("decrypt")} ${ansis.green("secret.json")} ${ansis.blue("-p pwd")} ${ansis.dim("\u89E3\u5BC6\u6587\u4EF6")}`
591
+ );
592
+ console.log(
593
+ ` ${ansis.yellow(CLI_ALIAS)} ${ansis.cyan("-i")} ${ansis.dim("\u4EA4\u4E92\u5F0F\u9009\u62E9\u529F\u80FD")}
594
+ `
595
+ );
596
+ }
597
+ async function runInteractiveMode() {
598
+ intro(ansis.bold.cyan(`\u{1F527} ${CLI_NAME}`));
599
+ const choice = await select({
600
+ message: "\u9009\u62E9\u529F\u80FD",
601
+ options: INTERACTIVE_OPTIONS
602
+ });
603
+ const selectedCommand = COMMAND_MAP[choice];
604
+ if (!selectedCommand) {
605
+ logger.error(`\u672A\u77E5\u547D\u4EE4: ${choice}`);
606
+ process.exit(1);
607
+ }
608
+ await selectedCommand.run?.({
609
+ rawArgs: ["-i"],
610
+ args: { _: [] },
611
+ cmd: {}
612
+ });
613
+ }
614
+ const main = defineCommand({
615
+ meta: {
616
+ name: "fkt",
617
+ version: "2.0.0",
618
+ description: `\u{1F527} ${CLI_NAME} - \u591A\u529F\u80FD\u6587\u4EF6\u5DE5\u5177\u7BB1`
619
+ },
620
+ args: {
621
+ interactive: {
622
+ type: "boolean",
623
+ alias: "i",
624
+ description: "\u8FDB\u5165\u4EA4\u4E92\u6A21\u5F0F",
625
+ default: false
626
+ }
627
+ },
628
+ // 子命令定义
629
+ subCommands: {
630
+ base64: () => base64,
631
+ restore: () => restore,
632
+ "video-to-audio": () => v2a,
633
+ v2a: () => v2a,
634
+ encrypt,
635
+ decrypt
636
+ },
637
+ // 默认行为
638
+ async run({ args, rawArgs }) {
639
+ if (args.interactive) {
640
+ await runInteractiveMode();
641
+ }
642
+ if (cliArgs.version) {
643
+ showVersion();
644
+ process.exit(0);
645
+ }
646
+ if (cliArgs.help || rawArgs.length === 0) {
647
+ showHelp();
648
+ process.exit(0);
649
+ }
650
+ }
651
+ });
652
+ preprocessArgs(process.argv);
653
+ runMain(main);