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,220 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { detectMutool, detectTool } from './detect';
5
+
6
+ export interface ImageConversionOptions {
7
+ pdfPath: string;
8
+ outputDir: string;
9
+ dpi?: number;
10
+ format?: 'png' | 'jpg';
11
+ basename?: string;
12
+ }
13
+
14
+ export async function convertPdfToImages(options: ImageConversionOptions): Promise<string[]> {
15
+ const { pdfPath, outputDir, dpi = 300, format = 'png', basename = 'page' } = options;
16
+
17
+ // Try mutool first
18
+ const mutool = await detectMutool();
19
+ if (mutool.available && mutool.path) {
20
+ return await convertWithMutool(mutool.path, pdfPath, outputDir, dpi, format, basename);
21
+ }
22
+
23
+ // Try pdftocairo (poppler)
24
+ const pdftocairo = await detectTool('pdftocairo');
25
+ if (pdftocairo.available && pdftocairo.path) {
26
+ return await convertWithPdftocairo(pdftocairo.path, pdfPath, outputDir, dpi, format, basename);
27
+ }
28
+
29
+ // Try pdftoppm (poppler alternative)
30
+ const pdftoppm = await detectTool('pdftoppm');
31
+ if (pdftoppm.available && pdftoppm.path) {
32
+ return await convertWithPdftoppm(pdftoppm.path, pdfPath, outputDir, dpi, format, basename);
33
+ }
34
+
35
+ throw new Error(
36
+ 'No PDF-to-image converter found.\n\n' +
37
+ 'Install one of these:\n' +
38
+ ' • mupdf-tools: brew install mupdf-tools (macOS) | apt install mupdf-tools (Linux)\n' +
39
+ ' • poppler: brew install poppler (macOS) | apt install poppler-utils (Linux)'
40
+ );
41
+ }
42
+
43
+ export async function extractPdfText(pdfPath: string, outputPath: string): Promise<void> {
44
+ // Try pdftotext (poppler)
45
+ const pdftotext = await detectTool('pdftotext');
46
+ if (pdftotext.available && pdftotext.path) {
47
+ const pdftotextPath = pdftotext.path;
48
+ return new Promise((resolve, reject) => {
49
+ const proc = spawn(pdftotextPath, ['-layout', pdfPath, outputPath], {
50
+ stdio: ['pipe', 'pipe', 'pipe'],
51
+ });
52
+
53
+ let stderr = '';
54
+ proc.stderr?.on('data', (data) => {
55
+ stderr += data.toString();
56
+ });
57
+
58
+ proc.on('close', (code) => {
59
+ if (code === 0) resolve();
60
+ else reject(new Error(`pdftotext failed: ${stderr}`));
61
+ });
62
+
63
+ proc.on('error', reject);
64
+ });
65
+ }
66
+
67
+ // Try mutool (mupdf)
68
+ const mutool = await detectMutool();
69
+ if (mutool.available && mutool.path) {
70
+ const mutoolPath = mutool.path;
71
+ return new Promise((resolve, reject) => {
72
+ // mutool draw -F text -o output.txt input.pdf
73
+ const proc = spawn(mutoolPath, ['draw', '-F', 'text', '-o', outputPath, pdfPath], {
74
+ stdio: ['pipe', 'pipe', 'pipe'],
75
+ });
76
+
77
+ let stderr = '';
78
+ proc.stderr?.on('data', (data) => {
79
+ stderr += data.toString();
80
+ });
81
+
82
+ proc.on('close', (code) => {
83
+ if (code === 0) resolve();
84
+ else reject(new Error(`mutool failed: ${stderr}`));
85
+ });
86
+
87
+ proc.on('error', reject);
88
+ });
89
+ }
90
+
91
+ throw new Error(
92
+ 'No PDF-to-text converter found.\n\n' +
93
+ 'Install one of these:\n' +
94
+ ' • poppler: brew install poppler (macOS) | apt install poppler-utils (Linux)\n' +
95
+ ' • mupdf-tools: brew install mupdf-tools (macOS) | apt install mupdf-tools (Linux)'
96
+ );
97
+ }
98
+
99
+ async function convertWithMutool(
100
+ mutoolPath: string,
101
+ pdfPath: string,
102
+ outputDir: string,
103
+ dpi: number,
104
+ format: 'png' | 'jpg',
105
+ basename: string
106
+ ): Promise<string[]> {
107
+ const outputPattern = join(outputDir, `${basename}-%d.${format}`);
108
+
109
+ return new Promise((resolve, reject) => {
110
+ const args = ['draw', '-o', outputPattern, '-r', dpi.toString(), pdfPath];
111
+ const proc = spawn(mutoolPath, args, { stdio: ['pipe', 'pipe', 'pipe'] });
112
+
113
+ let stderr = '';
114
+ proc.stderr?.on('data', (data) => {
115
+ stderr += data.toString();
116
+ });
117
+
118
+ proc.on('close', (code) => {
119
+ if (code === 0) {
120
+ try {
121
+ const files = readdirSync(outputDir)
122
+ .filter((f) => f.startsWith(basename) && f.endsWith(`.${format}`))
123
+ .sort((a, b) => {
124
+ const numA = Number.parseInt(a.match(/\d+/)?.[0] || '0', 10);
125
+ const numB = Number.parseInt(b.match(/\d+/)?.[0] || '0', 10);
126
+ return numA - numB;
127
+ })
128
+ .map((f) => join(outputDir, f));
129
+ resolve(files);
130
+ } catch (e) {
131
+ reject(e);
132
+ }
133
+ } else {
134
+ reject(new Error(`mutool failed: ${stderr}`));
135
+ }
136
+ });
137
+
138
+ proc.on('error', reject);
139
+ });
140
+ }
141
+
142
+ async function convertWithPdftocairo(
143
+ toolPath: string,
144
+ pdfPath: string,
145
+ outputDir: string,
146
+ dpi: number,
147
+ format: 'png' | 'jpg',
148
+ basename: string
149
+ ): Promise<string[]> {
150
+ const outputPrefix = join(outputDir, basename);
151
+ const fmtFlag = format === 'png' ? '-png' : '-jpeg';
152
+
153
+ return new Promise((resolve, reject) => {
154
+ const args = [fmtFlag, '-r', dpi.toString(), pdfPath, outputPrefix];
155
+ const proc = spawn(toolPath, args, { stdio: ['pipe', 'pipe', 'pipe'] });
156
+
157
+ let stderr = '';
158
+ proc.stderr?.on('data', (data) => {
159
+ stderr += data.toString();
160
+ });
161
+
162
+ proc.on('close', (code) => {
163
+ if (code === 0) {
164
+ try {
165
+ const files = readdirSync(outputDir)
166
+ .filter((f) => f.startsWith(basename) && f.endsWith(`.${format}`))
167
+ .sort()
168
+ .map((f) => join(outputDir, f));
169
+ resolve(files);
170
+ } catch (e) {
171
+ reject(e);
172
+ }
173
+ } else {
174
+ reject(new Error(`pdftocairo failed: ${stderr}`));
175
+ }
176
+ });
177
+
178
+ proc.on('error', reject);
179
+ });
180
+ }
181
+
182
+ async function convertWithPdftoppm(
183
+ toolPath: string,
184
+ pdfPath: string,
185
+ outputDir: string,
186
+ dpi: number,
187
+ format: 'png' | 'jpg',
188
+ basename: string
189
+ ): Promise<string[]> {
190
+ const outputPrefix = join(outputDir, basename);
191
+ const fmtFlag = format === 'png' ? '-png' : '-jpeg';
192
+
193
+ return new Promise((resolve, reject) => {
194
+ const args = [fmtFlag, '-r', dpi.toString(), pdfPath, outputPrefix];
195
+ const proc = spawn(toolPath, args, { stdio: ['pipe', 'pipe', 'pipe'] });
196
+
197
+ let stderr = '';
198
+ proc.stderr?.on('data', (data) => {
199
+ stderr += data.toString();
200
+ });
201
+
202
+ proc.on('close', (code) => {
203
+ if (code === 0) {
204
+ try {
205
+ const files = readdirSync(outputDir)
206
+ .filter((f) => f.startsWith(basename) && f.endsWith(`.${format}`))
207
+ .sort()
208
+ .map((f) => join(outputDir, f));
209
+ resolve(files);
210
+ } catch (e) {
211
+ reject(e);
212
+ }
213
+ } else {
214
+ reject(new Error(`pdftoppm failed: ${stderr}`));
215
+ }
216
+ });
217
+
218
+ proc.on('error', reject);
219
+ });
220
+ }
@@ -0,0 +1,142 @@
1
+ import cliProgress from 'cli-progress';
2
+ import ora, { type Ora } from 'ora';
3
+ import { c } from './style';
4
+
5
+ const IS_TTY = process.stdout.isTTY && !process.env.CI;
6
+
7
+ // Spinner for quick operations
8
+ export function createSpinner(text: string): Ora {
9
+ return ora({
10
+ text,
11
+ spinner: 'dots',
12
+ color: 'yellow', // ora doesn't support custom hex, 'yellow' is closest to our orange theme
13
+ isEnabled: IS_TTY,
14
+ });
15
+ }
16
+
17
+ // Progress bar for long operations
18
+ export function createProgressBar(format?: string): cliProgress.SingleBar {
19
+ return new cliProgress.SingleBar(
20
+ {
21
+ format:
22
+ format ||
23
+ ` ${c.active('{bar}')} ${c.dim('|')} {percentage}% ${c.dim('|')} {value}/{total} ${c.dim('|')} {task}`,
24
+ barCompleteChar: '█',
25
+ barIncompleteChar: '░',
26
+ hideCursor: true,
27
+ clearOnComplete: false,
28
+ stopOnComplete: true,
29
+ noTTYOutput: !IS_TTY,
30
+ },
31
+ cliProgress.Presets.shades_classic
32
+ );
33
+ }
34
+
35
+ // Multi-bar for parallel operations
36
+ export function createMultiBar(): cliProgress.MultiBar {
37
+ return new cliProgress.MultiBar(
38
+ {
39
+ format: ` ${c.active('{bar}')} ${c.dim('|')} {percentage}% ${c.dim('|')} {filename}`,
40
+ barCompleteChar: '█',
41
+ barIncompleteChar: '░',
42
+ hideCursor: true,
43
+ clearOnComplete: false,
44
+ stopOnComplete: true,
45
+ noTTYOutput: !IS_TTY,
46
+ },
47
+ cliProgress.Presets.shades_classic
48
+ );
49
+ }
50
+
51
+ // Simple task runner with spinner
52
+ export async function withSpinner<T>(
53
+ text: string,
54
+ task: () => Promise<T>,
55
+ successText?: string
56
+ ): Promise<T> {
57
+ // Non-TTY Fallback: Simple logging
58
+ if (!IS_TTY) {
59
+ console.log(`- ${text}`);
60
+ try {
61
+ const result = await task();
62
+ if (successText) console.log(`✔ ${successText}`);
63
+ return result;
64
+ } catch (err) {
65
+ console.error(`✖ ${text} failed`);
66
+ throw err;
67
+ }
68
+ }
69
+
70
+ const spinner = createSpinner(text);
71
+ spinner.start();
72
+
73
+ try {
74
+ const result = await task();
75
+ spinner.succeed(successText || text);
76
+ return result;
77
+ } catch (err) {
78
+ spinner.fail(text);
79
+ throw err;
80
+ }
81
+ }
82
+
83
+ // Progress wrapper for array operations
84
+ export async function withProgress<T, R>(
85
+ items: T[],
86
+ taskName: string,
87
+ processor: (item: T, index: number) => Promise<R>
88
+ ): Promise<R[]> {
89
+ // Non-TTY Fallback
90
+ if (!IS_TTY) {
91
+ console.log(`- ${taskName} (${items.length} items)...`);
92
+ const results: R[] = [];
93
+ for (let i = 0; i < items.length; i++) {
94
+ results.push(await processor(items[i], i));
95
+ }
96
+ console.log(`✔ ${taskName} complete`);
97
+ return results;
98
+ }
99
+
100
+ const bar = createProgressBar();
101
+ bar.start(items.length, 0, { task: taskName });
102
+
103
+ const results: R[] = [];
104
+
105
+ for (let i = 0; i < items.length; i++) {
106
+ const result = await processor(items[i], i);
107
+ results.push(result);
108
+ bar.update(i + 1, { task: `${taskName} (${i + 1}/${items.length})` });
109
+ }
110
+
111
+ bar.stop();
112
+ return results;
113
+ }
114
+
115
+ // Progress bar wrapper for batch operations
116
+ export async function withProgressBar<T>(
117
+ items: T[],
118
+ processor: (item: T, index: number) => Promise<void>,
119
+ options?: { label?: string }
120
+ ): Promise<void> {
121
+ const label = options?.label || 'Processing';
122
+
123
+ // Non-TTY Fallback
124
+ if (!IS_TTY) {
125
+ console.log(`- ${label} (${items.length} items)...`);
126
+ for (let i = 0; i < items.length; i++) {
127
+ await processor(items[i], i);
128
+ }
129
+ console.log(`✔ ${label} complete`);
130
+ return;
131
+ }
132
+
133
+ const bar = createProgressBar();
134
+ bar.start(items.length, 0, { task: label });
135
+
136
+ for (let i = 0; i < items.length; i++) {
137
+ await processor(items[i], i);
138
+ bar.update(i + 1, { task: `${label} (${i + 1}/${items.length})` });
139
+ }
140
+
141
+ bar.stop();
142
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * TUI Style Constants
3
+ * Colors and symbols for the terminal UI
4
+ */
5
+
6
+ import { hex, whiteBright } from './colors';
7
+
8
+ // Colors
9
+ export const c = {
10
+ title: hex('#f59e0b'), // Amber/orange - title
11
+ active: hex('#f59e0b'), // Amber/orange - active step, selected item
12
+ done: hex('#22c55e'), // Green - completed step, selected value
13
+ dim: hex('#6b7280'), // Gray - hints, unselected items
14
+ white: whiteBright, // Primary text (bright white)
15
+ error: hex('#ef4444'), // Red - error
16
+ warn: hex('#eab308'), // Yellow - warning
17
+ info: hex('#3b82f6'), // Blue - info
18
+
19
+ // Specific
20
+ modelId: hex('#4b5563'), // Dark gray
21
+ tagKey: hex('#22c55e'), // Green
22
+ tagNeedsKey: hex('#f59e0b'), // Amber/orange
23
+ };
24
+
25
+ // Symbols
26
+ export const sym = {
27
+ active: '■', // Current step indicator
28
+ done: '✓', // Completed step indicator
29
+ input: '◆', // Input step indicator
30
+ pointer: '>', // Selection cursor
31
+ selected: '●', // Selected item dot
32
+ unselected: '○', // Unselected item dot
33
+ cursor: '_', // Input cursor
34
+ mask: '•', // Password mask character
35
+ info: 'ℹ',
36
+ warn: '⚠',
37
+ error: '✖',
38
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ESNext"],
7
+ "types": ["bun-types"],
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+ "declaration": true,
17
+ "declarationMap": true,
18
+ "baseUrl": ".",
19
+ "rootDir": "./src",
20
+ "outDir": "./dist",
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": ["src/**/*"],
26
+ "exclude": ["node_modules", "dist"]
27
+ }