@seanmozeik/s3up 0.3.1

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.
@@ -0,0 +1,89 @@
1
+ // src/lib/archive.ts
2
+
3
+ import { readdir, stat } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ export interface ArchiveOptions {
7
+ compression: number;
8
+ name?: string;
9
+ }
10
+
11
+ export interface ArchiveResult {
12
+ blob: Blob;
13
+ name: string;
14
+ size: number;
15
+ }
16
+
17
+ export function generateArchiveName(directories: string[], override?: string): string {
18
+ if (override) {
19
+ return override.endsWith('.tar.gz') ? override : `${override}.tar.gz`;
20
+ }
21
+
22
+ const firstName = path.basename(directories[0]);
23
+ const date = new Date().toISOString().split('T')[0];
24
+ return `${firstName}-${date}.tar.gz`;
25
+ }
26
+
27
+ async function collectFiles(
28
+ dirPath: string,
29
+ baseName: string
30
+ ): Promise<Record<string, Uint8Array>> {
31
+ const files: Record<string, Uint8Array> = {};
32
+
33
+ async function walk(currentPath: string, relativePath: string): Promise<void> {
34
+ const entries = await readdir(currentPath, { withFileTypes: true });
35
+
36
+ for (const entry of entries) {
37
+ const fullPath = path.join(currentPath, entry.name);
38
+ const archivePath = path.join(relativePath, entry.name);
39
+
40
+ if (entry.isDirectory()) {
41
+ await walk(fullPath, archivePath);
42
+ } else if (entry.isFile()) {
43
+ const content = await Bun.file(fullPath).bytes();
44
+ files[archivePath] = content;
45
+ }
46
+ }
47
+ }
48
+
49
+ await walk(dirPath, baseName);
50
+ return files;
51
+ }
52
+
53
+ export async function createArchive(
54
+ directories: string[],
55
+ options: ArchiveOptions
56
+ ): Promise<ArchiveResult> {
57
+ const allFiles: Record<string, Uint8Array> = {};
58
+
59
+ // Collect files from all directories
60
+ for (const dir of directories) {
61
+ const baseName = path.basename(dir);
62
+ const dirFiles = await collectFiles(dir, baseName);
63
+ Object.assign(allFiles, dirFiles);
64
+ }
65
+
66
+ // Create tarball using Bun.Archive
67
+ const archive = new Bun.Archive(allFiles, {
68
+ compress: 'gzip',
69
+ level: options.compression
70
+ });
71
+
72
+ const blob = await archive.blob();
73
+ const name = generateArchiveName(directories, options.name);
74
+
75
+ return {
76
+ blob,
77
+ name,
78
+ size: blob.size
79
+ };
80
+ }
81
+
82
+ export async function isDirectory(filePath: string): Promise<boolean> {
83
+ try {
84
+ const stats = await stat(filePath);
85
+ return stats.isDirectory();
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
@@ -0,0 +1,41 @@
1
+ // src/lib/clipboard.ts
2
+ import { $ } from 'bun';
3
+
4
+ /**
5
+ * Copy text to clipboard (cross-platform)
6
+ * Returns true if successful, false if no clipboard tool available
7
+ */
8
+ export async function copyToClipboard(text: string): Promise<boolean> {
9
+ // macOS
10
+ if (process.platform === 'darwin') {
11
+ const proc = Bun.spawn(['pbcopy'], { stdin: 'pipe' });
12
+ proc.stdin.write(text);
13
+ proc.stdin.end();
14
+ await proc.exited;
15
+ return true;
16
+ }
17
+
18
+ // Linux: try available clipboard tools in order of preference
19
+ const tools: string[][] = [
20
+ ['xclip', '-selection', 'clipboard'],
21
+ ['xsel', '--clipboard', '--input'],
22
+ ['wl-copy'] // Wayland
23
+ ];
24
+
25
+ for (const cmd of tools) {
26
+ try {
27
+ const which = await $`which ${cmd[0]}`.quiet();
28
+ if (which.exitCode === 0) {
29
+ const proc = Bun.spawn(cmd, { stdin: 'pipe' });
30
+ proc.stdin.write(text);
31
+ proc.stdin.end();
32
+ await proc.exited;
33
+ return true;
34
+ }
35
+ } catch {
36
+ // Tool not found, try next
37
+ }
38
+ }
39
+
40
+ return false;
41
+ }
@@ -0,0 +1,72 @@
1
+ // src/lib/flags.test.ts
2
+ import { describe, expect, test } from 'bun:test';
3
+ import { parseGlobalFlags, parseUploadFlags } from './flags';
4
+
5
+ describe('parseGlobalFlags', () => {
6
+ test('parses --quiet flag', () => {
7
+ const result = parseGlobalFlags(['--quiet', 'file.txt']);
8
+ expect(result.quiet).toBe(true);
9
+ });
10
+
11
+ test('parses -q shorthand', () => {
12
+ const result = parseGlobalFlags(['-q', 'file.txt']);
13
+ expect(result.quiet).toBe(true);
14
+ });
15
+
16
+ test('parses --ci flag', () => {
17
+ const result = parseGlobalFlags(['--ci', 'file.txt']);
18
+ expect(result.ci).toBe(true);
19
+ });
20
+
21
+ test('returns remaining args', () => {
22
+ const result = parseGlobalFlags(['--quiet', '--ci', 'file.txt', 'file2.txt']);
23
+ expect(result.args).toEqual(['file.txt', 'file2.txt']);
24
+ });
25
+
26
+ test('handles no flags', () => {
27
+ const result = parseGlobalFlags(['file.txt']);
28
+ expect(result.quiet).toBe(false);
29
+ expect(result.ci).toBe(false);
30
+ expect(result.args).toEqual(['file.txt']);
31
+ });
32
+
33
+ test('passes through command-specific flags', () => {
34
+ const result = parseGlobalFlags(['prune', 'backups/', '--keep-last', '7', '--quiet']);
35
+ expect(result.quiet).toBe(true);
36
+ expect(result.args).toEqual(['prune', 'backups/', '--keep-last', '7']);
37
+ });
38
+ });
39
+
40
+ describe('parseUploadFlags', () => {
41
+ test('parses --prefix', () => {
42
+ const result = parseUploadFlags(['--prefix', 'backups/daily', 'file.txt']);
43
+ expect(result.prefix).toBe('backups/daily');
44
+ });
45
+
46
+ test('parses --compression', () => {
47
+ const result = parseUploadFlags(['--compression', '9', 'dir/']);
48
+ expect(result.compression).toBe(9);
49
+ });
50
+
51
+ test('parses --as', () => {
52
+ const result = parseUploadFlags(['--as', 'backup.tar.gz', 'dir/']);
53
+ expect(result.as).toBe('backup.tar.gz');
54
+ });
55
+
56
+ test('parses --fast and --slow', () => {
57
+ expect(parseUploadFlags(['--fast', 'file.txt']).fast).toBe(true);
58
+ expect(parseUploadFlags(['-f', 'file.txt']).fast).toBe(true);
59
+ expect(parseUploadFlags(['--slow', 'file.txt']).slow).toBe(true);
60
+ expect(parseUploadFlags(['-s', 'file.txt']).slow).toBe(true);
61
+ });
62
+
63
+ test('returns remaining paths', () => {
64
+ const result = parseUploadFlags(['--prefix', 'x', 'a.txt', 'b.txt']);
65
+ expect(result.paths).toEqual(['a.txt', 'b.txt']);
66
+ });
67
+
68
+ test('defaults compression to 6', () => {
69
+ const result = parseUploadFlags(['file.txt']);
70
+ expect(result.compression).toBe(6);
71
+ });
72
+ });
@@ -0,0 +1,129 @@
1
+ // src/lib/flags.ts
2
+
3
+ export interface GlobalFlags {
4
+ quiet: boolean;
5
+ ci: boolean;
6
+ help: boolean;
7
+ version: boolean;
8
+ args: string[];
9
+ }
10
+
11
+ export interface UploadFlags {
12
+ prefix?: string;
13
+ compression: number;
14
+ as?: string;
15
+ fast: boolean;
16
+ slow: boolean;
17
+ paths: string[];
18
+ }
19
+
20
+ export interface ListFlags {
21
+ json: boolean;
22
+ prefix?: string;
23
+ }
24
+
25
+ export interface PruneFlags {
26
+ olderThan?: number;
27
+ keepLast?: number;
28
+ minAge: string;
29
+ dryRun: boolean;
30
+ prefix: string;
31
+ }
32
+
33
+ export function parseGlobalFlags(args: string[]): GlobalFlags {
34
+ const result: GlobalFlags = {
35
+ args: [],
36
+ ci: false,
37
+ help: false,
38
+ quiet: false,
39
+ version: false
40
+ };
41
+
42
+ for (let i = 0; i < args.length; i++) {
43
+ const arg = args[i];
44
+ if (arg === '--quiet' || arg === '-q') {
45
+ result.quiet = true;
46
+ } else if (arg === '--ci') {
47
+ result.ci = true;
48
+ } else if (arg === '--help' || arg === '-h') {
49
+ result.help = true;
50
+ } else if (arg === '--version' || arg === '-v') {
51
+ result.version = true;
52
+ } else {
53
+ // Pass through everything else (including command-specific flags)
54
+ result.args.push(arg);
55
+ }
56
+ }
57
+
58
+ return result;
59
+ }
60
+
61
+ export function parseUploadFlags(args: string[]): UploadFlags {
62
+ const result: UploadFlags = {
63
+ compression: 6,
64
+ fast: false,
65
+ paths: [],
66
+ slow: false
67
+ };
68
+
69
+ for (let i = 0; i < args.length; i++) {
70
+ const arg = args[i];
71
+ if (arg === '--prefix' && args[i + 1]) {
72
+ result.prefix = args[++i];
73
+ } else if (arg === '--compression' && args[i + 1]) {
74
+ result.compression = parseInt(args[++i], 10);
75
+ } else if (arg === '--as' && args[i + 1]) {
76
+ result.as = args[++i];
77
+ } else if (arg === '--fast' || arg === '-f') {
78
+ result.fast = true;
79
+ } else if (arg === '--slow' || arg === '-s') {
80
+ result.slow = true;
81
+ } else if (!arg.startsWith('-')) {
82
+ result.paths.push(arg);
83
+ }
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ export function parseListFlags(args: string[]): ListFlags {
90
+ const result: ListFlags = {
91
+ json: false
92
+ };
93
+
94
+ for (let i = 0; i < args.length; i++) {
95
+ const arg = args[i];
96
+ if (arg === '--json') {
97
+ result.json = true;
98
+ } else if (!arg.startsWith('-')) {
99
+ result.prefix = arg;
100
+ }
101
+ }
102
+
103
+ return result;
104
+ }
105
+
106
+ export function parsePruneFlags(args: string[]): PruneFlags {
107
+ const result: PruneFlags = {
108
+ dryRun: false,
109
+ minAge: '1d',
110
+ prefix: ''
111
+ };
112
+
113
+ for (let i = 0; i < args.length; i++) {
114
+ const arg = args[i];
115
+ if (arg === '--older-than' && args[i + 1]) {
116
+ result.olderThan = parseInt(args[++i], 10);
117
+ } else if (arg === '--keep-last' && args[i + 1]) {
118
+ result.keepLast = parseInt(args[++i], 10);
119
+ } else if (arg === '--min-age' && args[i + 1]) {
120
+ result.minAge = args[++i];
121
+ } else if (arg === '--dry-run') {
122
+ result.dryRun = true;
123
+ } else if (!arg.startsWith('-')) {
124
+ result.prefix = arg;
125
+ }
126
+ }
127
+
128
+ return result;
129
+ }