trajectories-sh 1.0.2 → 1.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.
@@ -5,7 +5,7 @@ import {
5
5
  isLoggedIn,
6
6
  saveApiKey,
7
7
  saveSession
8
- } from "./chunk-JSCDM6ZO.js";
8
+ } from "./chunk-P6J5X7LP.js";
9
9
 
10
10
  // src/auth.ts
11
11
  import { createServer } from "http";
@@ -5,6 +5,8 @@ import { join } from "path";
5
5
  var CONFIG_DIR = join(homedir(), ".trajectories-sh");
6
6
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
7
  var DEFAULT_API_URL = "https://api.trajectories.sh";
8
+ var SUPABASE_URL = "https://rwghorglubtbjfzjotzu.supabase.co";
9
+ var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ3Z2hvcmdsdWJ0YmpmempvdHp1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzEzNzc5MjUsImV4cCI6MjA4Njk1MzkyNX0.AmFQnkLH0m-LfxepAnq3pmhObtHPhB1VEhd4aJQk5iA";
8
10
  function loadConfig() {
9
11
  if (!existsSync(CONFIG_FILE)) return {};
10
12
  try {
@@ -26,12 +28,53 @@ function getApiKey() {
26
28
  function getAccessToken() {
27
29
  return loadConfig().access_token;
28
30
  }
29
- function getAuthHeader() {
31
+ async function getAuthHeader() {
30
32
  const config = loadConfig();
31
33
  if (config.api_key) return `Bearer ${config.api_key}`;
32
- if (config.access_token) return `Bearer ${config.access_token}`;
34
+ if (config.access_token && config.refresh_token) {
35
+ const refreshed = await refreshTokenIfNeeded(config);
36
+ return `Bearer ${refreshed}`;
37
+ }
33
38
  return void 0;
34
39
  }
40
+ async function refreshTokenIfNeeded(config) {
41
+ const token = config.access_token;
42
+ try {
43
+ const payload = JSON.parse(
44
+ Buffer.from(token.split(".")[1], "base64").toString()
45
+ );
46
+ const nowSec = Math.floor(Date.now() / 1e3);
47
+ if (payload.exp && payload.exp > nowSec + 60) {
48
+ return token;
49
+ }
50
+ } catch {
51
+ }
52
+ try {
53
+ const resp = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
54
+ method: "POST",
55
+ headers: {
56
+ "Content-Type": "application/json",
57
+ apikey: SUPABASE_ANON_KEY
58
+ },
59
+ body: JSON.stringify({ refresh_token: config.refresh_token }),
60
+ signal: AbortSignal.timeout(1e4)
61
+ });
62
+ if (!resp.ok) {
63
+ throw new Error(`Refresh failed: ${resp.status}`);
64
+ }
65
+ const data = await resp.json();
66
+ saveSession({
67
+ access_token: data.access_token,
68
+ refresh_token: data.refresh_token,
69
+ email: config.user_email
70
+ });
71
+ return data.access_token;
72
+ } catch (e) {
73
+ throw new Error(
74
+ `Session expired. Run: trajectories auth login (${e.message})`
75
+ );
76
+ }
77
+ }
35
78
  function getUserEmail() {
36
79
  return loadConfig().user_email;
37
80
  }
package/dist/cli.js CHANGED
@@ -21,7 +21,7 @@ auth.command("login").description("Log in to trajectories.sh via your browser").
21
21
  if (opts.apiKey) {
22
22
  const spinner = ora("Validating API key...").start();
23
23
  try {
24
- const { loginWithApiKey } = await import("./auth-2SFY2XB2.js");
24
+ const { loginWithApiKey } = await import("./auth-UD3KTSHK.js");
25
25
  await loginWithApiKey(opts.apiKey);
26
26
  spinner.succeed("Logged in with API key");
27
27
  } catch (e) {
@@ -33,7 +33,7 @@ auth.command("login").description("Log in to trajectories.sh via your browser").
33
33
  console.log(chalk.bold("\n Opening browser to log in...\n"));
34
34
  console.log(chalk.dim(" If the browser doesn't open, visit:"));
35
35
  try {
36
- const { loginWithBrowser } = await import("./auth-2SFY2XB2.js");
36
+ const { loginWithBrowser } = await import("./auth-UD3KTSHK.js");
37
37
  const { email } = await loginWithBrowser();
38
38
  console.log(chalk.green(`
39
39
  \u2713 Logged in as ${email}
@@ -48,13 +48,13 @@ auth.command("login").description("Log in to trajectories.sh via your browser").
48
48
  });
49
49
  auth.command("logout").description("Log out and remove saved credentials").action(async () => {
50
50
  const chalk = (await import("chalk")).default;
51
- const { logout } = await import("./auth-2SFY2XB2.js");
51
+ const { logout } = await import("./auth-UD3KTSHK.js");
52
52
  logout();
53
53
  console.log(chalk.green(" \u2713 Logged out. Credentials removed."));
54
54
  });
55
55
  auth.command("status").description("Show current authentication status").action(async () => {
56
56
  const chalk = (await import("chalk")).default;
57
- const { status } = await import("./auth-2SFY2XB2.js");
57
+ const { status } = await import("./auth-UD3KTSHK.js");
58
58
  const s = status();
59
59
  if (s.loggedIn) {
60
60
  console.log(chalk.green(" \u2713 Authenticated"));
@@ -71,7 +71,7 @@ program.command("upload <directory>").description("Upload a trajectory job direc
71
71
  console.error(chalk.red(` \u2717 Directory not found: ${directory}`));
72
72
  process.exit(1);
73
73
  }
74
- const { isLoggedIn } = await import("./config-Y3GNDGUO.js");
74
+ const { isLoggedIn } = await import("./config-NOIAMV2U.js");
75
75
  if (!isLoggedIn()) {
76
76
  console.error(chalk.red(" \u2717 Not authenticated. Run: trajectories auth login"));
77
77
  process.exit(1);
@@ -81,7 +81,7 @@ program.command("upload <directory>").description("Upload a trajectory job direc
81
81
  Pushing ${directory} \u2192 ${chalk.cyan(slug)}
82
82
  `));
83
83
  try {
84
- const { uploadJob } = await import("./upload-CANGE4U5.js");
84
+ const { uploadJob } = await import("./upload-SELQA4FS.js");
85
85
  const result = await uploadJob(directory, {
86
86
  slug,
87
87
  name: opts.name,
@@ -103,8 +103,8 @@ program.command("upload <directory>").description("Upload a trajectory job direc
103
103
  });
104
104
  program.command("whoami").description("Show current authentication status").action(async () => {
105
105
  const chalk = (await import("chalk")).default;
106
- const { status } = await import("./auth-2SFY2XB2.js");
107
- const { getApiUrl } = await import("./config-Y3GNDGUO.js");
106
+ const { status } = await import("./auth-UD3KTSHK.js");
107
+ const { getApiUrl } = await import("./config-NOIAMV2U.js");
108
108
  const s = status();
109
109
  if (s.loggedIn) {
110
110
  console.log(chalk.green(" \u2713 Authenticated"));
@@ -11,7 +11,7 @@ import {
11
11
  saveApiKey,
12
12
  saveApiUrl,
13
13
  saveSession
14
- } from "./chunk-JSCDM6ZO.js";
14
+ } from "./chunk-P6J5X7LP.js";
15
15
  export {
16
16
  CONFIG_DIR,
17
17
  CONFIG_FILE,
@@ -0,0 +1,154 @@
1
+ import {
2
+ getApiUrl,
3
+ getAuthHeader
4
+ } from "./chunk-P6J5X7LP.js";
5
+
6
+ // src/upload.ts
7
+ import { readdirSync, statSync, readFileSync } from "fs";
8
+ import { join, relative } from "path";
9
+ var SKIP = /* @__PURE__ */ new Set(["__pycache__", ".DS_Store", ".git", "node_modules"]);
10
+ var MAX_FILE_SIZE = 50 * 1024 * 1024;
11
+ var CONCURRENCY = 15;
12
+ var BATCH_SIZE = 50;
13
+ function collectFiles(dir) {
14
+ const files = [];
15
+ function walk(current) {
16
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
17
+ if (SKIP.has(entry.name)) continue;
18
+ const full = join(current, entry.name);
19
+ if (entry.isDirectory()) {
20
+ walk(full);
21
+ } else if (entry.isFile()) {
22
+ const st = statSync(full);
23
+ if (st.size <= MAX_FILE_SIZE) {
24
+ files.push({ absPath: full, relPath: relative(dir, full), size: st.size });
25
+ }
26
+ }
27
+ }
28
+ }
29
+ walk(dir);
30
+ return files;
31
+ }
32
+ async function apiPost(path, body, timeout = 3e5) {
33
+ const auth = await getAuthHeader();
34
+ if (!auth) throw new Error("Not authenticated. Run: trajectories auth login");
35
+ return fetch(`${getApiUrl()}${path}`, {
36
+ method: "POST",
37
+ headers: { Authorization: auth, "Content-Type": "application/json" },
38
+ body: JSON.stringify(body),
39
+ signal: AbortSignal.timeout(timeout)
40
+ });
41
+ }
42
+ async function uploadJob(dir, opts) {
43
+ const slug = opts.slug ?? dir.replace(/\/+$/, "").split("/").pop();
44
+ const name = opts.name ?? slug;
45
+ const visibility = opts.visibility ?? "private";
46
+ const files = collectFiles(dir);
47
+ const totalSize = files.reduce((s, f) => s + f.size, 0);
48
+ console.log(` ${files.length} files, ${(totalSize / 1024 / 1024).toFixed(1)} MB`);
49
+ const initResp = await apiPost("/api/cli/push/init", { slug, name, visibility });
50
+ if (!initResp.ok) {
51
+ throw new Error(`Init failed: ${await initResp.text()}`);
52
+ }
53
+ const { job_id: jobId } = await initResp.json();
54
+ console.log(` Job ID: ${jobId}`);
55
+ await uploadDirect(jobId, files);
56
+ console.log(" Finalizing...");
57
+ const finalResp = await apiPost(`/api/cli/push/${jobId}/finalize`, {});
58
+ if (!finalResp.ok) {
59
+ throw new Error(`Finalize failed: ${await finalResp.text()}`);
60
+ }
61
+ const result = await finalResp.json();
62
+ return {
63
+ jobId,
64
+ slug: result.slug,
65
+ nTrials: result.n_trials,
66
+ meanReward: result.mean_reward,
67
+ viewerUrl: `${getApiUrl()}${result.viewer_url}`
68
+ };
69
+ }
70
+ async function uploadDirect(jobId, files) {
71
+ let uploaded = 0;
72
+ let errors = 0;
73
+ for (let batchStart = 0; batchStart < files.length; batchStart += BATCH_SIZE) {
74
+ const batch = files.slice(batchStart, batchStart + BATCH_SIZE);
75
+ const urlResp = await apiPost(`/api/cli/push/${jobId}/upload-urls`, {
76
+ paths: batch.map((f) => f.relPath)
77
+ });
78
+ if (!urlResp.ok) {
79
+ throw new Error(`Failed to get upload URLs: ${await urlResp.text()}`);
80
+ }
81
+ const { urls } = await urlResp.json();
82
+ const urlMap = /* @__PURE__ */ new Map();
83
+ for (const u of urls) {
84
+ if (u.signed_url) urlMap.set(u.path, u.signed_url);
85
+ else {
86
+ errors++;
87
+ console.log(` \u2717 ${u.path} (no signed URL: ${u.error})`);
88
+ }
89
+ }
90
+ const pending = [];
91
+ let active = 0;
92
+ for (const file of batch) {
93
+ const signedUrl = urlMap.get(file.relPath);
94
+ if (!signedUrl) continue;
95
+ const task = (async () => {
96
+ const ok = await uploadOneFile(file, signedUrl);
97
+ if (ok) {
98
+ uploaded++;
99
+ } else {
100
+ errors++;
101
+ console.log(` \u2717 ${file.relPath}`);
102
+ }
103
+ const total = uploaded + errors;
104
+ if (total % 10 === 0 || total === files.length) {
105
+ process.stdout.write(` \u2191 ${uploaded}/${files.length} uploaded\r`);
106
+ }
107
+ })();
108
+ pending.push(task);
109
+ active++;
110
+ if (active >= CONCURRENCY) {
111
+ await Promise.race(pending);
112
+ pending.splice(0, pending.findIndex((p) => {
113
+ let settled = false;
114
+ p.then(() => settled = true, () => settled = true);
115
+ return !settled;
116
+ }));
117
+ active = pending.length;
118
+ }
119
+ }
120
+ await Promise.all(pending);
121
+ }
122
+ console.log("");
123
+ if (errors > 0) {
124
+ console.log(` Uploaded: ${uploaded}, Errors: ${errors}`);
125
+ } else {
126
+ console.log(` \u2713 All ${uploaded} files uploaded directly to storage`);
127
+ }
128
+ }
129
+ async function uploadOneFile(file, signedUrl) {
130
+ const data = readFileSync(file.absPath);
131
+ for (let attempt = 0; attempt < 3; attempt++) {
132
+ try {
133
+ const resp = await fetch(signedUrl, {
134
+ method: "PUT",
135
+ headers: {
136
+ "Content-Type": "application/octet-stream"
137
+ },
138
+ body: data,
139
+ signal: AbortSignal.timeout(12e4)
140
+ });
141
+ if (resp.ok) return true;
142
+ } catch {
143
+ }
144
+ if (attempt < 2) await sleep(1e3 * (attempt + 1));
145
+ }
146
+ return false;
147
+ }
148
+ function sleep(ms) {
149
+ return new Promise((r) => setTimeout(r, ms));
150
+ }
151
+ export {
152
+ collectFiles,
153
+ uploadJob
154
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trajectories-sh",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "CLI for uploading trajectory jobs to trajectories.sh",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,261 +0,0 @@
1
- import {
2
- getApiUrl,
3
- getAuthHeader
4
- } from "./chunk-JSCDM6ZO.js";
5
-
6
- // src/upload.ts
7
- import { readdirSync, statSync, readFileSync } from "fs";
8
- import { join, relative } from "path";
9
- import { tmpdir } from "os";
10
- import { randomBytes } from "crypto";
11
- import { execFileSync } from "child_process";
12
- import * as tar from "tar";
13
- var SKIP = /* @__PURE__ */ new Set(["__pycache__", ".DS_Store", ".git", "node_modules"]);
14
- var MAX_FILE_SIZE = 50 * 1024 * 1024;
15
- var ARCHIVE_THRESHOLD = 50;
16
- var CHUNK_MAX_BYTES = 15 * 1024 * 1024;
17
- function collectFiles(dir) {
18
- const files = [];
19
- function walk(current) {
20
- for (const entry of readdirSync(current, { withFileTypes: true })) {
21
- if (SKIP.has(entry.name)) continue;
22
- const full = join(current, entry.name);
23
- if (entry.isDirectory()) {
24
- walk(full);
25
- } else if (entry.isFile()) {
26
- const st = statSync(full);
27
- if (st.size <= MAX_FILE_SIZE) {
28
- files.push({ absPath: full, relPath: relative(dir, full), size: st.size });
29
- }
30
- }
31
- }
32
- }
33
- walk(dir);
34
- return files;
35
- }
36
- async function apiPost(path, body, timeout = 3e5) {
37
- const auth = getAuthHeader();
38
- if (!auth) throw new Error("Not authenticated. Run: trajectories auth login");
39
- return fetch(`${getApiUrl()}${path}`, {
40
- method: "POST",
41
- headers: { Authorization: auth, "Content-Type": "application/json" },
42
- body: JSON.stringify(body),
43
- signal: AbortSignal.timeout(timeout)
44
- });
45
- }
46
- async function apiPostFile(path, formData) {
47
- const auth = getAuthHeader();
48
- if (!auth) throw new Error("Not authenticated. Run: trajectories auth login");
49
- return fetch(`${getApiUrl()}${path}`, {
50
- method: "POST",
51
- headers: { Authorization: auth },
52
- body: formData,
53
- signal: AbortSignal.timeout(3e5)
54
- });
55
- }
56
- async function apiGet(path) {
57
- const auth = getAuthHeader();
58
- if (!auth) throw new Error("Not authenticated. Run: trajectories auth login");
59
- return fetch(`${getApiUrl()}${path}`, {
60
- headers: { Authorization: auth },
61
- signal: AbortSignal.timeout(3e4)
62
- });
63
- }
64
- async function uploadJob(dir, opts) {
65
- const slug = opts.slug ?? dir.split("/").pop();
66
- const name = opts.name ?? slug;
67
- const visibility = opts.visibility ?? "private";
68
- const files = collectFiles(dir);
69
- const totalSize = files.reduce((s, f) => s + f.size, 0);
70
- console.log(` ${files.length} files, ${(totalSize / 1024 / 1024).toFixed(1)} MB`);
71
- const initResp = await apiPost("/api/cli/push/init", { slug, name, visibility });
72
- if (!initResp.ok) {
73
- const text = await initResp.text();
74
- throw new Error(`Init failed: ${text}`);
75
- }
76
- const { job_id: jobId } = await initResp.json();
77
- console.log(` Job ID: ${jobId}`);
78
- if (files.length >= ARCHIVE_THRESHOLD) {
79
- await uploadArchive(jobId, dir, files, totalSize);
80
- } else {
81
- await uploadFiles(jobId, files);
82
- }
83
- console.log(" Finalizing...");
84
- const finalResp = await apiPost(`/api/cli/push/${jobId}/finalize`, {});
85
- if (!finalResp.ok) {
86
- throw new Error(`Finalize failed: ${await finalResp.text()}`);
87
- }
88
- const result = await finalResp.json();
89
- return {
90
- jobId,
91
- slug: result.slug,
92
- nTrials: result.n_trials,
93
- meanReward: result.mean_reward,
94
- viewerUrl: `${getApiUrl()}${result.viewer_url}`
95
- };
96
- }
97
- async function uploadArchive(jobId, dir, files, totalSize) {
98
- const archivePath = join(tmpdir(), `trajectories-${randomBytes(4).toString("hex")}.tar.gz`);
99
- console.log(` Compressing ${files.length} files...`);
100
- await tar.create(
101
- { gzip: true, file: archivePath, cwd: dir },
102
- files.map((f) => f.relPath)
103
- );
104
- const archiveSize = statSync(archivePath).size;
105
- const ratio = ((1 - archiveSize / totalSize) * 100).toFixed(0);
106
- console.log(` Archive: ${(archiveSize / 1024 / 1024).toFixed(1)} MB (${ratio}% compressed)`);
107
- if (archiveSize <= CHUNK_MAX_BYTES) {
108
- await doSignedUploadAndExtract(jobId, archivePath, archiveSize);
109
- } else {
110
- await uploadChunked(jobId, dir, files, totalSize);
111
- }
112
- try {
113
- (await import("fs")).unlinkSync(archivePath);
114
- } catch {
115
- }
116
- }
117
- async function uploadChunked(jobId, dir, files, totalSize) {
118
- const targetRawPerChunk = CHUNK_MAX_BYTES * 4;
119
- const chunks = [[]];
120
- let currentSize = 0;
121
- for (const file of files) {
122
- if (currentSize + file.size > targetRawPerChunk && chunks[chunks.length - 1].length > 0) {
123
- chunks.push([]);
124
- currentSize = 0;
125
- }
126
- chunks[chunks.length - 1].push(file);
127
- currentSize += file.size;
128
- }
129
- console.log(` Splitting into ${chunks.length} chunks (max ${CHUNK_MAX_BYTES / 1024 / 1024}MB each)`);
130
- for (let i = 0; i < chunks.length; i++) {
131
- const chunk = chunks[i];
132
- const label = ` chunk ${i + 1}/${chunks.length}`;
133
- const chunkPath = join(tmpdir(), `trajectories-chunk-${i}-${randomBytes(4).toString("hex")}.tar.gz`);
134
- console.log(` Compressing${label} (${chunk.length} files)...`);
135
- await tar.create(
136
- { gzip: true, file: chunkPath, cwd: dir },
137
- chunk.map((f) => f.relPath)
138
- );
139
- const chunkSize = statSync(chunkPath).size;
140
- console.log(` ${label}: ${(chunkSize / 1024 / 1024).toFixed(1)} MB`);
141
- await doSignedUploadAndExtract(jobId, chunkPath, chunkSize, label);
142
- try {
143
- (await import("fs")).unlinkSync(chunkPath);
144
- } catch {
145
- }
146
- }
147
- }
148
- async function doSignedUploadAndExtract(jobId, archivePath, archiveSize, label = "") {
149
- const urlResp = await apiPost(`/api/cli/push/${jobId}/upload-url`, {});
150
- if (!urlResp.ok) throw new Error(`Failed to get upload URL: ${await urlResp.text()}`);
151
- const { signed_url: signedUrl } = await urlResp.json();
152
- console.log(" Uploading archive...");
153
- let uploaded = false;
154
- for (let attempt = 0; attempt < 3; attempt++) {
155
- try {
156
- try {
157
- const result = execFileSync("curl", [
158
- "--http1.1",
159
- "-s",
160
- "-w",
161
- "%{response_code}",
162
- "-o",
163
- "/dev/null",
164
- "-X",
165
- "PUT",
166
- signedUrl,
167
- "-H",
168
- "Content-Type: application/gzip",
169
- "-H",
170
- "Expect:",
171
- "--data-binary",
172
- `@${archivePath}`,
173
- "--max-time",
174
- "600"
175
- ], { encoding: "utf-8" });
176
- if (result.trim() === "200" || result.trim() === "201") {
177
- uploaded = true;
178
- break;
179
- }
180
- } catch {
181
- const body = readFileSync(archivePath);
182
- const resp = await fetch(signedUrl, {
183
- method: "PUT",
184
- headers: { "Content-Type": "application/gzip" },
185
- body,
186
- signal: AbortSignal.timeout(6e5)
187
- });
188
- if (resp.ok) {
189
- uploaded = true;
190
- break;
191
- }
192
- }
193
- } catch (e) {
194
- console.log(` Upload attempt ${attempt + 1} failed`);
195
- }
196
- if (attempt < 2) await sleep(5e3);
197
- }
198
- if (!uploaded) throw new Error("Archive upload failed after retries");
199
- console.log(" Extracting on server...");
200
- const extractResp = await apiPost(`/api/cli/push/${jobId}/extract`, {}, 3e4);
201
- if (!extractResp.ok) throw new Error(`Extract failed: ${await extractResp.text()}`);
202
- while (true) {
203
- await sleep(3e3);
204
- try {
205
- const statusResp = await apiGet(`/api/cli/push/${jobId}/extract-status`);
206
- if (!statusResp.ok) continue;
207
- const status = await statusResp.json();
208
- if (status.status === "done") {
209
- console.log(` \u2713 ${status.uploaded} files stored`);
210
- return;
211
- }
212
- if (status.status === "error") throw new Error(`Extraction error: ${status.detail}`);
213
- if (status.uploaded > 0) {
214
- process.stdout.write(` ${status.status}... ${status.uploaded} files stored\r`);
215
- }
216
- } catch (e) {
217
- if (e.message?.includes("Extraction error")) throw e;
218
- }
219
- }
220
- }
221
- async function uploadFiles(jobId, files) {
222
- let uploaded = 0;
223
- let errors = 0;
224
- for (const file of files) {
225
- let ok = false;
226
- for (let attempt = 0; attempt < 3; attempt++) {
227
- try {
228
- const formData = new FormData();
229
- const blob = new Blob([readFileSync(file.absPath)]);
230
- formData.append("file", blob, file.relPath);
231
- formData.append("path", file.relPath);
232
- const resp = await apiPostFile(`/api/cli/push/${jobId}/file`, formData);
233
- if (resp.ok) {
234
- ok = true;
235
- break;
236
- }
237
- } catch {
238
- }
239
- if (attempt < 2) await sleep(1e3 * (attempt + 1));
240
- }
241
- if (ok) {
242
- uploaded++;
243
- console.log(` \u2713 ${file.relPath}`);
244
- } else {
245
- errors++;
246
- console.log(` \u2717 ${file.relPath}`);
247
- }
248
- }
249
- if (errors > 0) {
250
- console.log(` Uploaded: ${uploaded}, Errors: ${errors}`);
251
- } else {
252
- console.log(` \u2713 All ${uploaded} files uploaded`);
253
- }
254
- }
255
- function sleep(ms) {
256
- return new Promise((r) => setTimeout(r, ms));
257
- }
258
- export {
259
- collectFiles,
260
- uploadJob
261
- };