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.
Files changed (74) hide show
  1. package/.github/workflows/release.yml +73 -0
  2. package/LICENSE +21 -0
  3. package/README.md +118 -0
  4. package/biome.json +34 -0
  5. package/bunfig.toml +7 -0
  6. package/dist/index.js +192 -0
  7. package/install.sh +68 -0
  8. package/package.json +47 -0
  9. package/scripts/inspect-help.ts +15 -0
  10. package/site/index.html +112 -0
  11. package/src/cli.ts +24 -0
  12. package/src/commands/audio/convert.ts +107 -0
  13. package/src/commands/audio/extract.ts +84 -0
  14. package/src/commands/audio/fade.ts +128 -0
  15. package/src/commands/audio/index.ts +35 -0
  16. package/src/commands/audio/merge.ts +109 -0
  17. package/src/commands/audio/normalize.ts +110 -0
  18. package/src/commands/audio/reverse.ts +64 -0
  19. package/src/commands/audio/speed.ts +101 -0
  20. package/src/commands/audio/trim.ts +98 -0
  21. package/src/commands/audio/volume.ts +91 -0
  22. package/src/commands/audio/waveform.ts +117 -0
  23. package/src/commands/doctor.ts +125 -0
  24. package/src/commands/image/adjust.ts +129 -0
  25. package/src/commands/image/border.ts +94 -0
  26. package/src/commands/image/bulk-compress.ts +111 -0
  27. package/src/commands/image/bulk-convert.ts +114 -0
  28. package/src/commands/image/bulk-resize.ts +112 -0
  29. package/src/commands/image/compress.ts +95 -0
  30. package/src/commands/image/convert.ts +116 -0
  31. package/src/commands/image/crop.ts +96 -0
  32. package/src/commands/image/favicon.ts +89 -0
  33. package/src/commands/image/filters.ts +108 -0
  34. package/src/commands/image/index.ts +49 -0
  35. package/src/commands/image/resize.ts +110 -0
  36. package/src/commands/image/rotate.ts +90 -0
  37. package/src/commands/image/strip-metadata.ts +60 -0
  38. package/src/commands/image/to-base64.ts +72 -0
  39. package/src/commands/image/watermark.ts +141 -0
  40. package/src/commands/pdf/compress.ts +157 -0
  41. package/src/commands/pdf/decrypt.ts +102 -0
  42. package/src/commands/pdf/delete-pages.ts +112 -0
  43. package/src/commands/pdf/duplicate.ts +119 -0
  44. package/src/commands/pdf/encrypt.ts +161 -0
  45. package/src/commands/pdf/from-images.ts +104 -0
  46. package/src/commands/pdf/index.ts +55 -0
  47. package/src/commands/pdf/merge.ts +84 -0
  48. package/src/commands/pdf/ocr.ts +270 -0
  49. package/src/commands/pdf/organize.ts +88 -0
  50. package/src/commands/pdf/page-numbers.ts +152 -0
  51. package/src/commands/pdf/reverse.ts +71 -0
  52. package/src/commands/pdf/rotate.ts +116 -0
  53. package/src/commands/pdf/sanitize.ts +77 -0
  54. package/src/commands/pdf/sign.ts +156 -0
  55. package/src/commands/pdf/split.ts +148 -0
  56. package/src/commands/pdf/to-images.ts +84 -0
  57. package/src/commands/pdf/to-text.ts +51 -0
  58. package/src/commands/pdf/watermark.ts +179 -0
  59. package/src/commands/qr/bulk-generate.ts +136 -0
  60. package/src/commands/qr/generate.ts +128 -0
  61. package/src/commands/qr/index.ts +16 -0
  62. package/src/commands/qr/scan.ts +114 -0
  63. package/src/commands/setup.ts +156 -0
  64. package/src/index.ts +42 -0
  65. package/src/lib/audio/ffmpeg.ts +93 -0
  66. package/src/utils/colors.ts +41 -0
  67. package/src/utils/detect.ts +222 -0
  68. package/src/utils/errors.ts +89 -0
  69. package/src/utils/files.ts +148 -0
  70. package/src/utils/logger.ts +90 -0
  71. package/src/utils/pdf-tools.ts +220 -0
  72. package/src/utils/progress.ts +142 -0
  73. package/src/utils/style.ts +38 -0
  74. 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
+ }