noupload 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/.github/workflows/release.yml +73 -0
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/biome.json +34 -0
- package/bunfig.toml +7 -0
- package/dist/index.js +192 -0
- package/install.sh +68 -0
- package/package.json +47 -0
- package/scripts/inspect-help.ts +15 -0
- package/site/index.html +112 -0
- package/src/cli.ts +24 -0
- package/src/commands/audio/convert.ts +107 -0
- package/src/commands/audio/extract.ts +84 -0
- package/src/commands/audio/fade.ts +128 -0
- package/src/commands/audio/index.ts +35 -0
- package/src/commands/audio/merge.ts +109 -0
- package/src/commands/audio/normalize.ts +110 -0
- package/src/commands/audio/reverse.ts +64 -0
- package/src/commands/audio/speed.ts +101 -0
- package/src/commands/audio/trim.ts +98 -0
- package/src/commands/audio/volume.ts +91 -0
- package/src/commands/audio/waveform.ts +117 -0
- package/src/commands/doctor.ts +125 -0
- package/src/commands/image/adjust.ts +129 -0
- package/src/commands/image/border.ts +94 -0
- package/src/commands/image/bulk-compress.ts +111 -0
- package/src/commands/image/bulk-convert.ts +114 -0
- package/src/commands/image/bulk-resize.ts +112 -0
- package/src/commands/image/compress.ts +95 -0
- package/src/commands/image/convert.ts +116 -0
- package/src/commands/image/crop.ts +96 -0
- package/src/commands/image/favicon.ts +89 -0
- package/src/commands/image/filters.ts +108 -0
- package/src/commands/image/index.ts +49 -0
- package/src/commands/image/resize.ts +110 -0
- package/src/commands/image/rotate.ts +90 -0
- package/src/commands/image/strip-metadata.ts +60 -0
- package/src/commands/image/to-base64.ts +72 -0
- package/src/commands/image/watermark.ts +141 -0
- package/src/commands/pdf/compress.ts +157 -0
- package/src/commands/pdf/decrypt.ts +102 -0
- package/src/commands/pdf/delete-pages.ts +112 -0
- package/src/commands/pdf/duplicate.ts +119 -0
- package/src/commands/pdf/encrypt.ts +161 -0
- package/src/commands/pdf/from-images.ts +104 -0
- package/src/commands/pdf/index.ts +55 -0
- package/src/commands/pdf/merge.ts +84 -0
- package/src/commands/pdf/ocr.ts +270 -0
- package/src/commands/pdf/organize.ts +88 -0
- package/src/commands/pdf/page-numbers.ts +152 -0
- package/src/commands/pdf/reverse.ts +71 -0
- package/src/commands/pdf/rotate.ts +116 -0
- package/src/commands/pdf/sanitize.ts +77 -0
- package/src/commands/pdf/sign.ts +156 -0
- package/src/commands/pdf/split.ts +148 -0
- package/src/commands/pdf/to-images.ts +84 -0
- package/src/commands/pdf/to-text.ts +51 -0
- package/src/commands/pdf/watermark.ts +179 -0
- package/src/commands/qr/bulk-generate.ts +136 -0
- package/src/commands/qr/generate.ts +128 -0
- package/src/commands/qr/index.ts +16 -0
- package/src/commands/qr/scan.ts +114 -0
- package/src/commands/setup.ts +156 -0
- package/src/index.ts +42 -0
- package/src/lib/audio/ffmpeg.ts +93 -0
- package/src/utils/colors.ts +41 -0
- package/src/utils/detect.ts +222 -0
- package/src/utils/errors.ts +89 -0
- package/src/utils/files.ts +148 -0
- package/src/utils/logger.ts +90 -0
- package/src/utils/pdf-tools.ts +220 -0
- package/src/utils/progress.ts +142 -0
- package/src/utils/style.ts +38 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ANSI color utilities
|
|
3
|
+
* Ported from claude-switch style
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Check if colors should be disabled
|
|
7
|
+
const NO_COLORS =
|
|
8
|
+
!process.stdout.isTTY || process.env.NO_COLOR !== undefined || process.env.TERM === 'dumb';
|
|
9
|
+
|
|
10
|
+
// Pass-through if colors disabled
|
|
11
|
+
const wrap = (code: string, reset = '0') =>
|
|
12
|
+
NO_COLORS ? (s: string) => s : (s: string) => `\x1b[${code}m${s}\x1b[${reset}m`;
|
|
13
|
+
|
|
14
|
+
// RGB color (24-bit)
|
|
15
|
+
const rgb = (r: number, g: number, b: number) => wrap(`38;2;${r};${g};${b}`);
|
|
16
|
+
|
|
17
|
+
// Hex color
|
|
18
|
+
export const hex = (h: string) => {
|
|
19
|
+
const r = Number.parseInt(h.slice(1, 3), 16);
|
|
20
|
+
const g = Number.parseInt(h.slice(3, 5), 16);
|
|
21
|
+
const b = Number.parseInt(h.slice(5, 7), 16);
|
|
22
|
+
return rgb(r, g, b);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Basic colors
|
|
26
|
+
export const red = wrap('31');
|
|
27
|
+
export const green = wrap('32');
|
|
28
|
+
export const yellow = wrap('33');
|
|
29
|
+
export const blue = wrap('34');
|
|
30
|
+
export const cyan = wrap('36');
|
|
31
|
+
export const white = wrap('37');
|
|
32
|
+
export const whiteBright = wrap('97');
|
|
33
|
+
export const gray = wrap('90');
|
|
34
|
+
|
|
35
|
+
// Modifiers
|
|
36
|
+
export const bold = wrap('1');
|
|
37
|
+
export const dim = wrap('2');
|
|
38
|
+
export const underline = wrap('4');
|
|
39
|
+
|
|
40
|
+
// Reset
|
|
41
|
+
export const reset = '\x1b[0m';
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { toolFound, toolNotFound } from './logger';
|
|
2
|
+
|
|
3
|
+
export interface ToolInfo {
|
|
4
|
+
available: boolean;
|
|
5
|
+
path?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Cache for detected tools
|
|
10
|
+
const toolCache = new Map<string, ToolInfo>();
|
|
11
|
+
|
|
12
|
+
// Detect if a tool is available on the system
|
|
13
|
+
export async function detectTool(name: string): Promise<ToolInfo> {
|
|
14
|
+
// Check cache first
|
|
15
|
+
if (toolCache.has(name)) {
|
|
16
|
+
const cached = toolCache.get(name);
|
|
17
|
+
if (cached) return cached;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const path = Bun.which(name);
|
|
22
|
+
if (path) {
|
|
23
|
+
const info: ToolInfo = { available: true, path };
|
|
24
|
+
toolCache.set(name, info);
|
|
25
|
+
return info;
|
|
26
|
+
}
|
|
27
|
+
throw new Error('Not found');
|
|
28
|
+
} catch {
|
|
29
|
+
const info: ToolInfo = { available: false };
|
|
30
|
+
toolCache.set(name, info);
|
|
31
|
+
return info;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Detect FFmpeg
|
|
36
|
+
export async function detectFFmpeg(): Promise<ToolInfo> {
|
|
37
|
+
return detectTool('ffmpeg');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Detect Ghostscript
|
|
41
|
+
export async function detectGhostscript(): Promise<ToolInfo> {
|
|
42
|
+
// Try different names based on platform
|
|
43
|
+
const names = ['gs', 'gswin64c', 'gswin32c'];
|
|
44
|
+
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
const info = await detectTool(name);
|
|
47
|
+
if (info.available) {
|
|
48
|
+
return info;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { available: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Detect mutool (MuPDF)
|
|
56
|
+
export async function detectMutool(): Promise<ToolInfo> {
|
|
57
|
+
return detectTool('mutool');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Detect qpdf
|
|
61
|
+
export async function detectQpdf(): Promise<ToolInfo> {
|
|
62
|
+
return detectTool('qpdf');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Detect ImageMagick
|
|
66
|
+
export async function detectImageMagick(): Promise<ToolInfo> {
|
|
67
|
+
const convert = await detectTool('convert');
|
|
68
|
+
if (convert.available) return convert;
|
|
69
|
+
return detectTool('magick');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get install command for a tool
|
|
73
|
+
export function getInstallCommand(tool: string): string | undefined {
|
|
74
|
+
// Return a generic help string for error messages
|
|
75
|
+
const help = getPackageMap()[tool.toLowerCase()];
|
|
76
|
+
if (!help) return undefined;
|
|
77
|
+
|
|
78
|
+
return `brew install ${help.brew} (macOS) | apt install ${help.apt} (Linux) | winget install ${help.winget} (Windows)`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface PackageMap {
|
|
82
|
+
brew: string;
|
|
83
|
+
apt: string;
|
|
84
|
+
dnf: string;
|
|
85
|
+
pacman: string;
|
|
86
|
+
apk: string;
|
|
87
|
+
winget: string;
|
|
88
|
+
choco?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getPackageMap(): Record<string, PackageMap> {
|
|
92
|
+
return {
|
|
93
|
+
ffmpeg: {
|
|
94
|
+
brew: 'ffmpeg',
|
|
95
|
+
apt: 'ffmpeg',
|
|
96
|
+
dnf: 'ffmpeg',
|
|
97
|
+
pacman: 'ffmpeg',
|
|
98
|
+
apk: 'ffmpeg',
|
|
99
|
+
winget: 'FFmpeg',
|
|
100
|
+
},
|
|
101
|
+
gs: {
|
|
102
|
+
brew: 'ghostscript',
|
|
103
|
+
apt: 'ghostscript',
|
|
104
|
+
dnf: 'ghostscript',
|
|
105
|
+
pacman: 'ghostscript',
|
|
106
|
+
apk: 'ghostscript',
|
|
107
|
+
winget: 'Ghostscript',
|
|
108
|
+
},
|
|
109
|
+
ghostscript: {
|
|
110
|
+
brew: 'ghostscript',
|
|
111
|
+
apt: 'ghostscript',
|
|
112
|
+
dnf: 'ghostscript',
|
|
113
|
+
pacman: 'ghostscript',
|
|
114
|
+
apk: 'ghostscript',
|
|
115
|
+
winget: 'Ghostscript',
|
|
116
|
+
},
|
|
117
|
+
mutool: {
|
|
118
|
+
brew: 'mupdf-tools',
|
|
119
|
+
apt: 'mupdf-tools',
|
|
120
|
+
dnf: 'mupdf',
|
|
121
|
+
pacman: 'mupdf-tools',
|
|
122
|
+
apk: 'mupdf-tools',
|
|
123
|
+
winget: 'MuPDF',
|
|
124
|
+
},
|
|
125
|
+
mupdf: {
|
|
126
|
+
brew: 'mupdf-tools',
|
|
127
|
+
apt: 'mupdf-tools',
|
|
128
|
+
dnf: 'mupdf',
|
|
129
|
+
pacman: 'mupdf-tools',
|
|
130
|
+
apk: 'mupdf-tools',
|
|
131
|
+
winget: 'MuPDF',
|
|
132
|
+
},
|
|
133
|
+
qpdf: {
|
|
134
|
+
brew: 'qpdf',
|
|
135
|
+
apt: 'qpdf',
|
|
136
|
+
dnf: 'qpdf',
|
|
137
|
+
pacman: 'qpdf',
|
|
138
|
+
apk: 'qpdf',
|
|
139
|
+
winget: 'QPDF',
|
|
140
|
+
},
|
|
141
|
+
convert: {
|
|
142
|
+
brew: 'imagemagick',
|
|
143
|
+
apt: 'imagemagick',
|
|
144
|
+
dnf: 'imagemagick',
|
|
145
|
+
pacman: 'imagemagick',
|
|
146
|
+
apk: 'imagemagick',
|
|
147
|
+
winget: 'ImageMagick',
|
|
148
|
+
},
|
|
149
|
+
magick: {
|
|
150
|
+
brew: 'imagemagick',
|
|
151
|
+
apt: 'imagemagick',
|
|
152
|
+
dnf: 'imagemagick',
|
|
153
|
+
pacman: 'imagemagick',
|
|
154
|
+
apk: 'imagemagick',
|
|
155
|
+
winget: 'ImageMagick',
|
|
156
|
+
},
|
|
157
|
+
imagemagick: {
|
|
158
|
+
brew: 'imagemagick',
|
|
159
|
+
apt: 'imagemagick',
|
|
160
|
+
dnf: 'imagemagick',
|
|
161
|
+
pacman: 'imagemagick',
|
|
162
|
+
apk: 'imagemagick',
|
|
163
|
+
winget: 'ImageMagick',
|
|
164
|
+
},
|
|
165
|
+
tesseract: {
|
|
166
|
+
brew: 'tesseract',
|
|
167
|
+
apt: 'tesseract-ocr',
|
|
168
|
+
dnf: 'tesseract',
|
|
169
|
+
pacman: 'tesseract',
|
|
170
|
+
apk: 'tesseract-ocr',
|
|
171
|
+
winget: 'Tesseract-OCR',
|
|
172
|
+
},
|
|
173
|
+
pdftotext: {
|
|
174
|
+
brew: 'poppler',
|
|
175
|
+
apt: 'poppler-utils',
|
|
176
|
+
dnf: 'poppler-utils',
|
|
177
|
+
pacman: 'poppler',
|
|
178
|
+
apk: 'poppler-utils',
|
|
179
|
+
winget: 'Poppler',
|
|
180
|
+
},
|
|
181
|
+
poppler: {
|
|
182
|
+
brew: 'poppler',
|
|
183
|
+
apt: 'poppler-utils',
|
|
184
|
+
dnf: 'poppler-utils',
|
|
185
|
+
pacman: 'poppler',
|
|
186
|
+
apk: 'poppler-utils',
|
|
187
|
+
winget: 'Poppler',
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check and report tool availability
|
|
193
|
+
export async function checkTool(name: string, silent = false): Promise<boolean> {
|
|
194
|
+
const info = await detectTool(name);
|
|
195
|
+
|
|
196
|
+
if (!silent) {
|
|
197
|
+
if (info.available && info.path) {
|
|
198
|
+
toolFound(name, info.path);
|
|
199
|
+
} else {
|
|
200
|
+
toolNotFound(name, getInstallCommand(name));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return info.available;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Get best available PDF compressor
|
|
208
|
+
export async function getBestPdfCompressor(): Promise<'ghostscript' | 'mutool' | 'builtin'> {
|
|
209
|
+
const gs = await detectGhostscript();
|
|
210
|
+
if (gs.available) return 'ghostscript';
|
|
211
|
+
|
|
212
|
+
const mutool = await detectMutool();
|
|
213
|
+
if (mutool.available) return 'mutool';
|
|
214
|
+
|
|
215
|
+
return 'builtin';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Get best available audio processor
|
|
219
|
+
export async function getBestAudioProcessor(): Promise<'ffmpeg' | 'wasm'> {
|
|
220
|
+
const ffmpeg = await detectFFmpeg();
|
|
221
|
+
return ffmpeg.available ? 'ffmpeg' : 'wasm';
|
|
222
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { error as logError } from './logger';
|
|
2
|
+
import { c } from './style';
|
|
3
|
+
|
|
4
|
+
export class NoUploadError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
public suggestion?: string,
|
|
8
|
+
public code?: string
|
|
9
|
+
) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'NoUploadError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class FileNotFoundError extends NoUploadError {
|
|
16
|
+
constructor(path: string) {
|
|
17
|
+
super(
|
|
18
|
+
`File not found: ${path}`,
|
|
19
|
+
'Check if the file path is correct and the file exists.',
|
|
20
|
+
'FILE_NOT_FOUND'
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class InvalidFileTypeError extends NoUploadError {
|
|
26
|
+
constructor(path: string, expected: string[]) {
|
|
27
|
+
super(
|
|
28
|
+
`Invalid file type: ${path}`,
|
|
29
|
+
`Expected file types: ${expected.join(', ')}`,
|
|
30
|
+
'INVALID_FILE_TYPE'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ToolNotFoundError extends NoUploadError {
|
|
36
|
+
constructor(tool: string, installCmd?: string) {
|
|
37
|
+
super(
|
|
38
|
+
`Required tool not found: ${tool}`,
|
|
39
|
+
installCmd ? `Install with: ${installCmd}` : undefined,
|
|
40
|
+
'TOOL_NOT_FOUND'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class ProcessingError extends NoUploadError {
|
|
46
|
+
constructor(message: string, suggestion?: string) {
|
|
47
|
+
super(message, suggestion, 'PROCESSING_ERROR');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Error handler for CLI
|
|
52
|
+
export function handleError(err: unknown): never {
|
|
53
|
+
if (err instanceof NoUploadError) {
|
|
54
|
+
logError(err.message, err.suggestion);
|
|
55
|
+
} else if (err instanceof Error) {
|
|
56
|
+
logError(err.message);
|
|
57
|
+
if (process.env.DEBUG) {
|
|
58
|
+
console.error(c.dim(err.stack || ''));
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
logError(String(err));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Wrap async function with error handling
|
|
68
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic function constraint
|
|
69
|
+
export function withErrorHandling<T extends (...args: any[]) => Promise<any>>(fn: T): T {
|
|
70
|
+
return (async (...args: Parameters<T>) => {
|
|
71
|
+
try {
|
|
72
|
+
return await fn(...args);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
handleError(err);
|
|
75
|
+
}
|
|
76
|
+
}) as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Validate required arguments
|
|
80
|
+
export function requireArg<T>(value: T | undefined, name: string, message?: string): T {
|
|
81
|
+
if (value === undefined || value === null || value === '') {
|
|
82
|
+
throw new NoUploadError(
|
|
83
|
+
message || `Missing required argument: ${name}`,
|
|
84
|
+
`Provide the ${name} argument.`,
|
|
85
|
+
'MISSING_ARG'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Ensure directory exists
|
|
5
|
+
export function ensureDir(path: string): void {
|
|
6
|
+
const dir = dirname(path);
|
|
7
|
+
if (!existsSync(dir)) {
|
|
8
|
+
mkdirSync(dir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Ensure output directory exists
|
|
13
|
+
export function ensureOutputDir(outputPath: string): void {
|
|
14
|
+
if (outputPath.endsWith('/') || !extname(outputPath)) {
|
|
15
|
+
// It's a directory
|
|
16
|
+
if (!existsSync(outputPath)) {
|
|
17
|
+
mkdirSync(outputPath, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
// It's a file, ensure parent directory exists
|
|
21
|
+
ensureDir(outputPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get file size in bytes
|
|
26
|
+
export function getFileSize(path: string): number {
|
|
27
|
+
try {
|
|
28
|
+
return statSync(path).size;
|
|
29
|
+
} catch {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if file exists
|
|
35
|
+
export function fileExists(path: string): boolean {
|
|
36
|
+
return existsSync(path);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check if path is a directory
|
|
40
|
+
export function isDirectory(path: string): boolean {
|
|
41
|
+
try {
|
|
42
|
+
return statSync(path).isDirectory();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get file extension without dot
|
|
49
|
+
export function getExtension(path: string): string {
|
|
50
|
+
return extname(path).slice(1).toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get filename without extension
|
|
54
|
+
export function getBasename(path: string): string {
|
|
55
|
+
return basename(path, extname(path));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Generate output path
|
|
59
|
+
export function generateOutputPath(
|
|
60
|
+
inputPath: string,
|
|
61
|
+
outputArg: string | undefined,
|
|
62
|
+
suffix?: string,
|
|
63
|
+
newExt?: string
|
|
64
|
+
): string {
|
|
65
|
+
const inputBasename = getBasename(inputPath);
|
|
66
|
+
const inputExt = getExtension(inputPath);
|
|
67
|
+
const finalExt = newExt || inputExt;
|
|
68
|
+
const finalName = suffix ? `${inputBasename}${suffix}` : inputBasename;
|
|
69
|
+
|
|
70
|
+
if (!outputArg) {
|
|
71
|
+
// No output specified, use input directory with suffix
|
|
72
|
+
const dir = dirname(inputPath);
|
|
73
|
+
return join(dir, `${finalName}.${finalExt}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isDirectory(outputArg) || outputArg.endsWith('/')) {
|
|
77
|
+
// Output is a directory
|
|
78
|
+
ensureOutputDir(outputArg);
|
|
79
|
+
return join(outputArg, `${finalName}.${finalExt}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Output is a file path
|
|
83
|
+
ensureDir(outputArg);
|
|
84
|
+
return outputArg;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Glob files with pattern
|
|
88
|
+
export async function globFiles(
|
|
89
|
+
patterns: string | string[],
|
|
90
|
+
options?: { cwd?: string; absolute?: boolean }
|
|
91
|
+
): Promise<string[]> {
|
|
92
|
+
const patternList = Array.isArray(patterns) ? patterns : [patterns];
|
|
93
|
+
const cwd = options?.cwd || process.cwd();
|
|
94
|
+
const absolute = options?.absolute ?? true;
|
|
95
|
+
const results: string[] = [];
|
|
96
|
+
|
|
97
|
+
for (const pattern of patternList) {
|
|
98
|
+
const glob = new Bun.Glob(pattern);
|
|
99
|
+
// Bun.Glob.scan returns AsyncIterable
|
|
100
|
+
for await (const file of glob.scan({ cwd, absolute })) {
|
|
101
|
+
results.push(file);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate input files exist
|
|
108
|
+
export function validateInputFiles(files: string[]): string[] {
|
|
109
|
+
const missing: string[] = [];
|
|
110
|
+
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
if (!fileExists(file)) {
|
|
113
|
+
missing.push(file);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return missing;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Resolve path (handle relative paths)
|
|
121
|
+
export function resolvePath(path: string): string {
|
|
122
|
+
return resolve(path);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Get relative path from cwd
|
|
126
|
+
export function getRelativePath(path: string): string {
|
|
127
|
+
const cwd = process.cwd();
|
|
128
|
+
if (path.startsWith(cwd)) {
|
|
129
|
+
return path.slice(cwd.length + 1);
|
|
130
|
+
}
|
|
131
|
+
return path;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Supported file extensions by category
|
|
135
|
+
export const SUPPORTED_EXTENSIONS = {
|
|
136
|
+
pdf: ['pdf'],
|
|
137
|
+
image: ['jpg', 'jpeg', 'png', 'webp', 'gif', 'tiff', 'tif', 'avif', 'heic', 'heif', 'svg'],
|
|
138
|
+
audio: ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus'],
|
|
139
|
+
video: ['mp4', 'mkv', 'avi', 'mov', 'webm', 'flv', 'wmv'],
|
|
140
|
+
qr: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg'],
|
|
141
|
+
} as const;
|
|
142
|
+
|
|
143
|
+
// Check if file is of expected type
|
|
144
|
+
export function isFileType(path: string, category: keyof typeof SUPPORTED_EXTENSIONS): boolean {
|
|
145
|
+
const ext = getExtension(path);
|
|
146
|
+
const extensions = SUPPORTED_EXTENSIONS[category] as readonly string[];
|
|
147
|
+
return extensions.includes(ext);
|
|
148
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { filesize } from 'filesize';
|
|
2
|
+
import { c, sym } from './style';
|
|
3
|
+
|
|
4
|
+
// Styled output helpers
|
|
5
|
+
export function success(message: string): void {
|
|
6
|
+
console.log(`${c.done(sym.done)} ${message}`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function error(message: string, suggestion?: string): void {
|
|
10
|
+
console.error(`${c.error(sym.error)} ${c.error(message)}`);
|
|
11
|
+
if (suggestion) {
|
|
12
|
+
console.error(` ${c.dim('Hint:')} ${suggestion}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function warn(message: string): void {
|
|
17
|
+
console.warn(`${c.warn(sym.warn)} ${c.warn(message)}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function info(message: string): void {
|
|
21
|
+
console.log(`${c.info(sym.info)} ${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function dim(message: string): void {
|
|
25
|
+
console.log(c.dim(message));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// File operation result display
|
|
29
|
+
export function fileResult(
|
|
30
|
+
input: string,
|
|
31
|
+
output: string,
|
|
32
|
+
stats?: { before: number; after: number }
|
|
33
|
+
): void {
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(` ${c.dim('Input')} ${input}`);
|
|
36
|
+
console.log(` ${c.done('Output')} ${output}`);
|
|
37
|
+
|
|
38
|
+
if (stats) {
|
|
39
|
+
const reduction = ((1 - stats.after / stats.before) * 100).toFixed(1);
|
|
40
|
+
const beforeSize = filesize(stats.before) as string;
|
|
41
|
+
const afterSize = filesize(stats.after) as string;
|
|
42
|
+
const saved = stats.before > stats.after;
|
|
43
|
+
|
|
44
|
+
const change = saved ? c.done(`-${reduction}%`) : c.warn(`+${Math.abs(Number(reduction))}%`);
|
|
45
|
+
|
|
46
|
+
console.log(` ${c.dim('Size')} ${beforeSize} → ${afterSize} (${change})`);
|
|
47
|
+
}
|
|
48
|
+
console.log();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Bulk operation result
|
|
52
|
+
export function bulkResult(processed: number, failed: number, totalSaved?: number): void {
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(c.dim(' ─────────────────────────────'));
|
|
55
|
+
console.log(` ${c.dim('Processed')} ${c.done(String(processed))}`);
|
|
56
|
+
|
|
57
|
+
if (failed > 0) {
|
|
58
|
+
console.log(` ${c.dim('Failed')} ${c.error(String(failed))}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (totalSaved !== undefined) {
|
|
62
|
+
const savedSize = filesize(totalSaved) as string;
|
|
63
|
+
console.log(` ${c.dim('Saved')} ${c.active(savedSize)}`);
|
|
64
|
+
}
|
|
65
|
+
console.log();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Tool detection message
|
|
69
|
+
export function toolFound(tool: string, path: string): void {
|
|
70
|
+
console.log(`${c.done(sym.done)} ${tool} ${c.dim(path)}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function toolNotFound(tool: string, installCmd?: string): void {
|
|
74
|
+
console.warn(`${c.error(sym.error)} ${tool} ${c.dim('not found')}`);
|
|
75
|
+
if (installCmd) {
|
|
76
|
+
console.log(` ${c.active('Install:')} ${installCmd}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Section header
|
|
81
|
+
export function header(title: string): void {
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(c.title(title));
|
|
84
|
+
console.log();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Format bytes
|
|
88
|
+
export function formatBytes(bytes: number): string {
|
|
89
|
+
return filesize(bytes) as string;
|
|
90
|
+
}
|