@vargai/sdk 0.1.2 → 0.2.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/cli/commands/upload.ts +215 -0
- package/cli/index.ts +3 -1
- package/package.json +2 -2
- package/utilities/s3.ts +2 -2
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* varg upload command
|
|
3
|
+
* upload local files to cloudflare r2 storage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, statSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { defineCommand } from "citty";
|
|
9
|
+
import { uploadFile } from "../../utilities/s3";
|
|
10
|
+
import { box, c, formatDuration, row, success, error } from "../ui";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get file type category from extension
|
|
14
|
+
*/
|
|
15
|
+
function getFileType(ext: string): string {
|
|
16
|
+
const normalized = ext.toLowerCase();
|
|
17
|
+
|
|
18
|
+
const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"];
|
|
19
|
+
const videoExts = [".mp4", ".mpeg", ".mpg", ".mov", ".avi", ".webm", ".mkv"];
|
|
20
|
+
const audioExts = [".mp3", ".wav", ".ogg", ".flac", ".aac", ".m4a"];
|
|
21
|
+
|
|
22
|
+
if (imageExts.includes(normalized)) return "image";
|
|
23
|
+
if (videoExts.includes(normalized)) return "video";
|
|
24
|
+
if (audioExts.includes(normalized)) return "audio";
|
|
25
|
+
|
|
26
|
+
return "file";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create URL-safe slug from filename
|
|
31
|
+
*/
|
|
32
|
+
function slugify(text: string, maxLength = 40): string {
|
|
33
|
+
return text
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[\s_]+/g, "-")
|
|
36
|
+
.replace(/[^\w-]/g, "")
|
|
37
|
+
.replace(/-+/g, "-")
|
|
38
|
+
.replace(/^-|-$/g, "")
|
|
39
|
+
.slice(0, maxLength)
|
|
40
|
+
.replace(/-$/, "") || "file";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Format file size in human readable format
|
|
45
|
+
*/
|
|
46
|
+
function formatSize(bytes: number): string {
|
|
47
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
48
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
49
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
50
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
51
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate S3 object key from file path and optional destination
|
|
56
|
+
*/
|
|
57
|
+
function generateObjectKey(filePath: string, destination?: string): string {
|
|
58
|
+
const filename = path.basename(filePath);
|
|
59
|
+
|
|
60
|
+
if (destination) {
|
|
61
|
+
// If ends with "/", append original filename
|
|
62
|
+
if (destination.endsWith("/")) {
|
|
63
|
+
return `${destination}${filename}`;
|
|
64
|
+
}
|
|
65
|
+
// Use destination as-is (full path specified)
|
|
66
|
+
return destination;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Auto-generate path: uploads/{type}s/{slug}_{uuid}.{ext}
|
|
70
|
+
const ext = path.extname(filePath);
|
|
71
|
+
const basename = path.basename(filePath, ext);
|
|
72
|
+
const fileType = getFileType(ext);
|
|
73
|
+
const slug = slugify(basename);
|
|
74
|
+
const uuid = crypto.randomUUID().slice(0, 8);
|
|
75
|
+
|
|
76
|
+
return `uploads/${fileType}s/${slug}_${uuid}${ext}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const uploadCmd = defineCommand({
|
|
80
|
+
meta: {
|
|
81
|
+
name: "upload",
|
|
82
|
+
description: "upload local file to cloud storage",
|
|
83
|
+
},
|
|
84
|
+
args: {
|
|
85
|
+
file: {
|
|
86
|
+
type: "positional",
|
|
87
|
+
description: "local file path to upload",
|
|
88
|
+
required: true,
|
|
89
|
+
},
|
|
90
|
+
destination: {
|
|
91
|
+
type: "string",
|
|
92
|
+
alias: "d",
|
|
93
|
+
description:
|
|
94
|
+
"s3 destination path (e.g. users/123/video.mp4 or projects/abc/)",
|
|
95
|
+
},
|
|
96
|
+
json: {
|
|
97
|
+
type: "boolean",
|
|
98
|
+
description: "output result as json",
|
|
99
|
+
},
|
|
100
|
+
quiet: {
|
|
101
|
+
type: "boolean",
|
|
102
|
+
description: "minimal output",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
async run({ args }) {
|
|
106
|
+
const { file: filePath, destination, json, quiet } = args;
|
|
107
|
+
|
|
108
|
+
// Validate file exists
|
|
109
|
+
if (!filePath || !existsSync(filePath)) {
|
|
110
|
+
if (json) {
|
|
111
|
+
console.log(
|
|
112
|
+
JSON.stringify({ success: false, error: "file not found" }),
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
console.error(`${c.red("error:")} file not found: ${filePath}`);
|
|
116
|
+
}
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Get file info
|
|
121
|
+
const stats = statSync(filePath);
|
|
122
|
+
const filename = path.basename(filePath);
|
|
123
|
+
const ext = path.extname(filePath);
|
|
124
|
+
const fileType = getFileType(ext);
|
|
125
|
+
const fileSize = stats.size;
|
|
126
|
+
|
|
127
|
+
// Generate object key
|
|
128
|
+
const objectKey = generateObjectKey(filePath, destination);
|
|
129
|
+
|
|
130
|
+
// Show progress
|
|
131
|
+
if (!quiet && !json) {
|
|
132
|
+
const content = [
|
|
133
|
+
"",
|
|
134
|
+
row("file", filename),
|
|
135
|
+
row("size", formatSize(fileSize)),
|
|
136
|
+
row("type", fileType),
|
|
137
|
+
"",
|
|
138
|
+
` ${c.cyan("◐")} uploading...`,
|
|
139
|
+
"",
|
|
140
|
+
];
|
|
141
|
+
console.log(box("upload", content));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const startTime = Date.now();
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const url = await uploadFile(filePath, objectKey);
|
|
148
|
+
const elapsed = Date.now() - startTime;
|
|
149
|
+
|
|
150
|
+
if (json) {
|
|
151
|
+
console.log(
|
|
152
|
+
JSON.stringify({
|
|
153
|
+
success: true,
|
|
154
|
+
file: filename,
|
|
155
|
+
size: fileSize,
|
|
156
|
+
type: fileType,
|
|
157
|
+
destination: objectKey,
|
|
158
|
+
url,
|
|
159
|
+
time: elapsed,
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
} else if (quiet) {
|
|
163
|
+
console.log(url);
|
|
164
|
+
} else {
|
|
165
|
+
// Clear and show result
|
|
166
|
+
console.log("\x1b[2J\x1b[H");
|
|
167
|
+
|
|
168
|
+
const content = [
|
|
169
|
+
"",
|
|
170
|
+
row("file", filename),
|
|
171
|
+
row("size", formatSize(fileSize)),
|
|
172
|
+
row("type", fileType),
|
|
173
|
+
"",
|
|
174
|
+
success(`uploaded in ${formatDuration(elapsed)}`),
|
|
175
|
+
"",
|
|
176
|
+
row("destination", objectKey),
|
|
177
|
+
"",
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
console.log(box("upload", content));
|
|
181
|
+
console.log(`\n ${c.cyan("url")} ${url}\n`);
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
const elapsed = Date.now() - startTime;
|
|
185
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
186
|
+
|
|
187
|
+
if (json) {
|
|
188
|
+
console.log(
|
|
189
|
+
JSON.stringify({ success: false, error: errorMsg, time: elapsed }),
|
|
190
|
+
);
|
|
191
|
+
} else if (quiet) {
|
|
192
|
+
console.error(errorMsg);
|
|
193
|
+
} else {
|
|
194
|
+
console.log("\x1b[2J\x1b[H");
|
|
195
|
+
|
|
196
|
+
const content = [
|
|
197
|
+
"",
|
|
198
|
+
row("file", filename),
|
|
199
|
+
row("size", formatSize(fileSize)),
|
|
200
|
+
row("type", fileType),
|
|
201
|
+
"",
|
|
202
|
+
error("upload failed"),
|
|
203
|
+
"",
|
|
204
|
+
row("error", errorMsg),
|
|
205
|
+
"",
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
console.log(box("upload", content));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
package/cli/index.ts
CHANGED
|
@@ -10,12 +10,13 @@ import { findCmd } from "./commands/find";
|
|
|
10
10
|
import { helpCmd } from "./commands/help";
|
|
11
11
|
import { listCmd } from "./commands/list";
|
|
12
12
|
import { runCmd } from "./commands/run";
|
|
13
|
+
import { uploadCmd } from "./commands/upload";
|
|
13
14
|
import { whichCmd } from "./commands/which";
|
|
14
15
|
|
|
15
16
|
const main = defineCommand({
|
|
16
17
|
meta: {
|
|
17
18
|
name: "varg",
|
|
18
|
-
version: "0.
|
|
19
|
+
version: "0.2.0",
|
|
19
20
|
description: "ai video infrastructure from your terminal",
|
|
20
21
|
},
|
|
21
22
|
subCommands: {
|
|
@@ -26,6 +27,7 @@ const main = defineCommand({
|
|
|
26
27
|
search: findCmd,
|
|
27
28
|
which: whichCmd,
|
|
28
29
|
inspect: whichCmd,
|
|
30
|
+
upload: uploadCmd,
|
|
29
31
|
help: helpCmd,
|
|
30
32
|
},
|
|
31
33
|
});
|
package/package.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@higgsfield/client": "^0.1.2",
|
|
43
43
|
"@remotion/cli": "^4.0.377",
|
|
44
44
|
"@types/fluent-ffmpeg": "^2.1.28",
|
|
45
|
-
"@vargai/sdk": "^0.1.
|
|
45
|
+
"@vargai/sdk": "^0.1.2",
|
|
46
46
|
"ai": "^5.0.98",
|
|
47
47
|
"citty": "^0.1.6",
|
|
48
48
|
"fluent-ffmpeg": "^2.1.3",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"remotion": "^4.0.377",
|
|
53
53
|
"replicate": "^1.4.0"
|
|
54
54
|
},
|
|
55
|
-
"version": "0.
|
|
55
|
+
"version": "0.2.0",
|
|
56
56
|
"exports": {
|
|
57
57
|
".": "./index.ts"
|
|
58
58
|
}
|
package/utilities/s3.ts
CHANGED
|
@@ -80,10 +80,10 @@ export function getPublicUrl(objectKey: string): string {
|
|
|
80
80
|
const endpoint = process.env.CLOUDFLARE_R2_API_URL || "";
|
|
81
81
|
|
|
82
82
|
if (endpoint.includes("localhost")) {
|
|
83
|
-
return `${endpoint}/${
|
|
83
|
+
return `${endpoint}/${objectKey}`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
return `
|
|
86
|
+
return `https://s3.varg.ai/${objectKey}`;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// cli runner
|