cutsio 0.2.4 → 0.2.6

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 (2) hide show
  1. package/dist/index.js +241 -185
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -137,6 +137,18 @@ var ApiError = class extends Error {
137
137
  this.status = status;
138
138
  }
139
139
  };
140
+ var MAX_REQUEST_RETRIES = 3;
141
+ var BASE_RETRY_DELAY_MS = 1e3;
142
+ var MAX_RETRY_DELAY_MS = 3e4;
143
+ function retryDelayMs(attempt, retryAfter) {
144
+ const headerSeconds = retryAfter ? Number(retryAfter) : Number.NaN;
145
+ const headerMs = Number.isFinite(headerSeconds) && headerSeconds >= 0 ? headerSeconds * 1e3 : null;
146
+ const backoffMs = Math.min(BASE_RETRY_DELAY_MS * 2 ** attempt, MAX_RETRY_DELAY_MS);
147
+ return Math.min(headerMs ?? backoffMs, MAX_RETRY_DELAY_MS) + Math.random() * 500;
148
+ }
149
+ function sleep(ms) {
150
+ return new Promise((resolve) => setTimeout(resolve, ms));
151
+ }
140
152
  var CutsioClient = class {
141
153
  base;
142
154
  apiKey;
@@ -181,17 +193,21 @@ var CutsioClient = class {
181
193
  ...init?.body ? { "Content-Type": "application/json" } : {},
182
194
  ...init?.headers ?? {}
183
195
  };
184
- const response = await fetch(`${this.base}${path}`, { ...init, headers });
185
- if (!response.ok) {
186
- let message = `HTTP ${response.status}`;
187
- try {
188
- const body = await response.json();
189
- if (typeof body.error === "string") message = body.error;
190
- } catch {
196
+ for (let attempt = 0; ; attempt += 1) {
197
+ const response = await fetch(`${this.base}${path}`, { ...init, headers });
198
+ if (response.ok) return response.json();
199
+ const retryable = response.status === 429 || response.status === 502 || response.status === 503 || response.status === 504;
200
+ if (!retryable || attempt >= MAX_REQUEST_RETRIES) {
201
+ let message = `HTTP ${response.status}`;
202
+ try {
203
+ const body = await response.json();
204
+ if (typeof body.error === "string") message = body.error;
205
+ } catch {
206
+ }
207
+ throw new ApiError(message, response.status);
191
208
  }
192
- throw new ApiError(message, response.status);
209
+ await sleep(retryDelayMs(attempt, response.headers.get("retry-after")));
193
210
  }
194
- return response.json();
195
211
  }
196
212
  };
197
213
 
@@ -350,6 +366,195 @@ function resolveSelection(manifest, selection) {
350
366
  return { videos: [...selected.values()], errors };
351
367
  }
352
368
 
369
+ // src/ui/format.ts
370
+ function formatBytes(bytes) {
371
+ const units = ["B", "KB", "MB", "GB", "TB"];
372
+ let value = bytes;
373
+ let unit = 0;
374
+ while (value >= 1024 && unit < units.length - 1) {
375
+ value /= 1024;
376
+ unit += 1;
377
+ }
378
+ return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
379
+ }
380
+
381
+ // src/ui/theme.ts
382
+ var colorEnabled = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
383
+ function paint(code) {
384
+ return colorEnabled ? (text) => `\x1B[${code}m${text}\x1B[0m` : (text) => text;
385
+ }
386
+ var color = {
387
+ dim: paint(2),
388
+ bold: paint(1),
389
+ red: paint(31),
390
+ green: paint(32),
391
+ yellow: paint(33),
392
+ blue: paint(34),
393
+ magenta: paint(35),
394
+ cyan: paint(36)
395
+ };
396
+ var isInteractive = colorEnabled;
397
+ var animationsEnabled = colorEnabled && process.env.CUTSIO_NO_ANIM !== "1";
398
+ function sleep2(ms) {
399
+ return new Promise((resolve) => setTimeout(resolve, ms));
400
+ }
401
+
402
+ // src/ui/progress.ts
403
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
404
+ var BAR_WIDTH = 22;
405
+ var ProgressView = class {
406
+ active = /* @__PURE__ */ new Map();
407
+ interactive = isInteractive;
408
+ statusLine = null;
409
+ renderedLines = 0;
410
+ frame = 0;
411
+ timer = null;
412
+ dirty = false;
413
+ constructor() {
414
+ if (!this.interactive) return;
415
+ this.timer = setInterval(() => {
416
+ if (this.active.size === 0 && !this.statusLine) return;
417
+ this.frame += 1;
418
+ this.render();
419
+ }, 120);
420
+ this.timer.unref();
421
+ }
422
+ status(text) {
423
+ if (!this.interactive) return;
424
+ this.statusLine = text;
425
+ this.dirty = true;
426
+ this.render();
427
+ }
428
+ /** Print a line above the live area without tearing it. */
429
+ log(message) {
430
+ if (!this.interactive) {
431
+ console.log(message);
432
+ return;
433
+ }
434
+ this.clear();
435
+ process.stdout.write(`${message}
436
+ `);
437
+ this.render();
438
+ }
439
+ error(message) {
440
+ if (!this.interactive) {
441
+ console.error(message);
442
+ return;
443
+ }
444
+ this.clear();
445
+ process.stderr.write(`${message}
446
+ `);
447
+ this.render();
448
+ }
449
+ start(id, label) {
450
+ if (!this.interactive) {
451
+ console.log(` fetch ${label}`);
452
+ return;
453
+ }
454
+ const now = Date.now();
455
+ this.active.set(id, {
456
+ label,
457
+ bytes: 0,
458
+ total: null,
459
+ lastBytes: 0,
460
+ lastAt: now,
461
+ speed: 0
462
+ });
463
+ this.dirty = true;
464
+ this.render();
465
+ }
466
+ progress(id, bytes, total) {
467
+ if (!this.interactive) return;
468
+ const item = this.active.get(id);
469
+ if (!item) return;
470
+ const now = Date.now();
471
+ const elapsed = (now - item.lastAt) / 1e3;
472
+ if (elapsed >= 0.3) {
473
+ const delta = bytes - item.lastBytes;
474
+ if (delta >= 0) item.speed = delta / elapsed;
475
+ item.lastBytes = bytes;
476
+ item.lastAt = now;
477
+ }
478
+ item.bytes = bytes;
479
+ if (total !== null) item.total = total;
480
+ this.dirty = true;
481
+ }
482
+ finish(id, ok, text) {
483
+ if (!this.interactive) {
484
+ if (ok) console.log(` done ${text}`);
485
+ else console.error(` FAILED ${text}`);
486
+ return;
487
+ }
488
+ this.clear();
489
+ const line = ok ? ` ${color.green("\u2713")} ${text}` : ` ${color.red("\u2717")} ${text}`;
490
+ (ok ? process.stdout : process.stderr).write(`${line}
491
+ `);
492
+ this.active.delete(id);
493
+ this.render();
494
+ }
495
+ stop() {
496
+ if (this.timer) clearInterval(this.timer);
497
+ this.timer = null;
498
+ if (!this.interactive) return;
499
+ this.clear();
500
+ this.active.clear();
501
+ this.statusLine = null;
502
+ }
503
+ /** Run `work` while a spinner status shows; cleared on exit, plain line when piped. */
504
+ async withStatus(text, work) {
505
+ if (!this.interactive) {
506
+ console.log(text);
507
+ try {
508
+ return await work();
509
+ } finally {
510
+ this.stop();
511
+ }
512
+ }
513
+ this.status(text);
514
+ try {
515
+ return await work();
516
+ } finally {
517
+ this.statusLine = null;
518
+ this.render();
519
+ }
520
+ }
521
+ clear() {
522
+ if (this.renderedLines === 0) return;
523
+ process.stdout.write(`\x1B[${this.renderedLines}A\x1B[J`);
524
+ this.renderedLines = 0;
525
+ }
526
+ render() {
527
+ if (!this.interactive) return;
528
+ if (this.active.size === 0 && !this.statusLine) {
529
+ this.clear();
530
+ return;
531
+ }
532
+ const lines = [];
533
+ if (this.statusLine) {
534
+ const spinner = FRAMES[this.frame % FRAMES.length] ?? FRAMES[0];
535
+ lines.push(` ${color.cyan(spinner ?? "")} ${color.dim(this.statusLine)}`);
536
+ }
537
+ for (const item of this.active.values()) {
538
+ lines.push(` ${color.bold(item.label)}`);
539
+ lines.push(` ${this.bar(item)}`);
540
+ }
541
+ this.clear();
542
+ process.stdout.write(`${lines.join("\n")}
543
+ `);
544
+ this.renderedLines = lines.length;
545
+ this.dirty = false;
546
+ }
547
+ bar(item) {
548
+ const ratio = item.total && item.total > 0 ? Math.min(1, item.bytes / item.total) : null;
549
+ const filled = ratio === null ? 0 : Math.round(ratio * BAR_WIDTH);
550
+ const track = color.cyan("\u2588".repeat(filled)) + color.dim("\u2591".repeat(BAR_WIDTH - filled));
551
+ const percent = ratio === null ? " ? " : `${Math.floor(ratio * 100)}%`.padStart(4);
552
+ const size = item.total ? `${formatBytes(item.bytes)} / ${formatBytes(item.total)}` : formatBytes(item.bytes);
553
+ const speed = item.speed > 0 ? `${formatBytes(item.speed)}/s` : "";
554
+ return `${track} ${percent} ${size} ${color.dim(speed)}`.trimEnd();
555
+ }
556
+ };
557
+
353
558
  // src/commands/list.ts
354
559
  async function listCommand(flags) {
355
560
  const { apiKey, migrated } = await resolveApiKey();
@@ -359,12 +564,15 @@ async function listCommand(flags) {
359
564
  return 1;
360
565
  }
361
566
  const client = new CutsioClient(resolveApiUrl(flags.url), apiKey);
567
+ const view = new ProgressView();
362
568
  let manifest;
363
569
  try {
364
- manifest = await client.fetchManifest();
570
+ manifest = await view.withStatus("Fetching workspace\u2026", () => client.fetchManifest());
365
571
  } catch (error) {
366
572
  console.error(error instanceof Error ? error.message : String(error));
367
573
  return 1;
574
+ } finally {
575
+ view.stop();
368
576
  }
369
577
  if (flags.json === "true") {
370
578
  console.log(JSON.stringify(manifest, null, 2));
@@ -446,7 +654,16 @@ async function loginCommand(flags) {
446
654
  continue;
447
655
  }
448
656
  try {
449
- const who = await new CutsioClient(apiUrl, apiKey).whoami();
657
+ const view = new ProgressView();
658
+ let who;
659
+ try {
660
+ who = await view.withStatus(
661
+ "Verifying key\u2026",
662
+ () => new CutsioClient(apiUrl, apiKey).whoami()
663
+ );
664
+ } finally {
665
+ view.stop();
666
+ }
450
667
  if (!who.orgId) {
451
668
  console.error("That key is not attached to an organization. Create an organization API key.\n");
452
669
  continue;
@@ -751,12 +968,16 @@ async function sync(client, options) {
751
968
  while (queue.size > 0) {
752
969
  const ready = [];
753
970
  const ids = [...queue.keys()];
971
+ let serverDelayMs = 0;
754
972
  for (let index = 0; index < ids.length; index += PREPARE_BATCH_SIZE) {
755
973
  const batch = ids.slice(index, index + PREPARE_BATCH_SIZE);
756
974
  const results = await client.prepare(batch);
757
975
  for (const result of results) {
758
976
  const item = queue.get(result.id);
759
977
  if (!item) continue;
978
+ if (typeof result.retryAfterSeconds === "number" && result.retryAfterSeconds > 0) {
979
+ serverDelayMs = Math.max(serverDelayMs, result.retryAfterSeconds * 1e3);
980
+ }
760
981
  switch (result.status) {
761
982
  case "ready":
762
983
  if (result.url) {
@@ -850,7 +1071,7 @@ async function sync(client, options) {
850
1071
  }
851
1072
  break;
852
1073
  }
853
- await sleep(prepareDelayMs);
1074
+ await sleep3(Math.min(Math.max(serverDelayMs, prepareDelayMs), MAX_PREPARE_DELAY_MS));
854
1075
  prepareDelayMs = Math.min(prepareDelayMs * 2, MAX_PREPARE_DELAY_MS);
855
1076
  }
856
1077
  return { downloaded, unchanged, skipped, failed };
@@ -870,180 +1091,10 @@ async function runPool(items, concurrency, worker) {
870
1091
  })
871
1092
  );
872
1093
  }
873
- function sleep(ms) {
874
- return new Promise((resolve) => setTimeout(resolve, ms));
875
- }
876
-
877
- // src/ui/format.ts
878
- function formatBytes(bytes) {
879
- const units = ["B", "KB", "MB", "GB", "TB"];
880
- let value = bytes;
881
- let unit = 0;
882
- while (value >= 1024 && unit < units.length - 1) {
883
- value /= 1024;
884
- unit += 1;
885
- }
886
- return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
887
- }
888
-
889
- // src/ui/theme.ts
890
- var colorEnabled = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
891
- function paint(code) {
892
- return colorEnabled ? (text) => `\x1B[${code}m${text}\x1B[0m` : (text) => text;
893
- }
894
- var color = {
895
- dim: paint(2),
896
- bold: paint(1),
897
- red: paint(31),
898
- green: paint(32),
899
- yellow: paint(33),
900
- blue: paint(34),
901
- magenta: paint(35),
902
- cyan: paint(36)
903
- };
904
- var isInteractive = colorEnabled;
905
- var animationsEnabled = colorEnabled && process.env.CUTSIO_NO_ANIM !== "1";
906
- function sleep2(ms) {
1094
+ function sleep3(ms) {
907
1095
  return new Promise((resolve) => setTimeout(resolve, ms));
908
1096
  }
909
1097
 
910
- // src/ui/progress.ts
911
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
912
- var BAR_WIDTH = 22;
913
- var ProgressView = class {
914
- active = /* @__PURE__ */ new Map();
915
- interactive = isInteractive;
916
- statusLine = null;
917
- renderedLines = 0;
918
- frame = 0;
919
- timer = null;
920
- dirty = false;
921
- constructor() {
922
- if (!this.interactive) return;
923
- this.timer = setInterval(() => {
924
- if (this.active.size === 0) return;
925
- this.frame += 1;
926
- this.render();
927
- }, 120);
928
- this.timer.unref();
929
- }
930
- status(text) {
931
- if (!this.interactive) return;
932
- this.statusLine = text;
933
- this.dirty = true;
934
- }
935
- /** Print a line above the live area without tearing it. */
936
- log(message) {
937
- if (!this.interactive) {
938
- console.log(message);
939
- return;
940
- }
941
- this.clear();
942
- process.stdout.write(`${message}
943
- `);
944
- this.render();
945
- }
946
- error(message) {
947
- if (!this.interactive) {
948
- console.error(message);
949
- return;
950
- }
951
- this.clear();
952
- process.stderr.write(`${message}
953
- `);
954
- this.render();
955
- }
956
- start(id, label) {
957
- if (!this.interactive) {
958
- console.log(` fetch ${label}`);
959
- return;
960
- }
961
- const now = Date.now();
962
- this.active.set(id, {
963
- label,
964
- bytes: 0,
965
- total: null,
966
- lastBytes: 0,
967
- lastAt: now,
968
- speed: 0
969
- });
970
- this.dirty = true;
971
- this.render();
972
- }
973
- progress(id, bytes, total) {
974
- if (!this.interactive) return;
975
- const item = this.active.get(id);
976
- if (!item) return;
977
- const now = Date.now();
978
- const elapsed = (now - item.lastAt) / 1e3;
979
- if (elapsed >= 0.3) {
980
- const delta = bytes - item.lastBytes;
981
- if (delta >= 0) item.speed = delta / elapsed;
982
- item.lastBytes = bytes;
983
- item.lastAt = now;
984
- }
985
- item.bytes = bytes;
986
- if (total !== null) item.total = total;
987
- this.dirty = true;
988
- }
989
- finish(id, ok, text) {
990
- if (!this.interactive) {
991
- if (ok) console.log(` done ${text}`);
992
- else console.error(` FAILED ${text}`);
993
- return;
994
- }
995
- this.clear();
996
- const line = ok ? ` ${color.green("\u2713")} ${text}` : ` ${color.red("\u2717")} ${text}`;
997
- (ok ? process.stdout : process.stderr).write(`${line}
998
- `);
999
- this.active.delete(id);
1000
- this.render();
1001
- }
1002
- stop() {
1003
- if (this.timer) clearInterval(this.timer);
1004
- this.timer = null;
1005
- if (!this.interactive) return;
1006
- this.clear();
1007
- this.active.clear();
1008
- this.statusLine = null;
1009
- }
1010
- clear() {
1011
- if (this.renderedLines === 0) return;
1012
- process.stdout.write(`\x1B[${this.renderedLines}A\x1B[J`);
1013
- this.renderedLines = 0;
1014
- }
1015
- render() {
1016
- if (!this.interactive) return;
1017
- if (this.active.size === 0 && !this.statusLine) {
1018
- this.clear();
1019
- return;
1020
- }
1021
- const lines = [];
1022
- if (this.statusLine) {
1023
- const spinner = FRAMES[this.frame % FRAMES.length] ?? FRAMES[0];
1024
- lines.push(` ${color.cyan(spinner ?? "")} ${color.dim(this.statusLine)}`);
1025
- }
1026
- for (const item of this.active.values()) {
1027
- lines.push(` ${color.bold(item.label)}`);
1028
- lines.push(` ${this.bar(item)}`);
1029
- }
1030
- this.clear();
1031
- process.stdout.write(`${lines.join("\n")}
1032
- `);
1033
- this.renderedLines = lines.length;
1034
- this.dirty = false;
1035
- }
1036
- bar(item) {
1037
- const ratio = item.total && item.total > 0 ? Math.min(1, item.bytes / item.total) : null;
1038
- const filled = ratio === null ? 0 : Math.round(ratio * BAR_WIDTH);
1039
- const track = color.cyan("\u2588".repeat(filled)) + color.dim("\u2591".repeat(BAR_WIDTH - filled));
1040
- const percent = ratio === null ? " ? " : `${Math.floor(ratio * 100)}%`.padStart(4);
1041
- const size = item.total ? `${formatBytes(item.bytes)} / ${formatBytes(item.total)}` : formatBytes(item.bytes);
1042
- const speed = item.speed > 0 ? `${formatBytes(item.speed)}/s` : "";
1043
- return `${track} ${percent} ${size} ${color.dim(speed)}`.trimEnd();
1044
- }
1045
- };
1046
-
1047
1098
  // src/commands/selection-prompt.ts
1048
1099
  import { stdin as stdin2, stdout as stdout2 } from "node:process";
1049
1100
  import { createInterface } from "node:readline/promises";
@@ -1172,7 +1223,7 @@ async function syncCommand(flags) {
1172
1223
  try {
1173
1224
  if (flags.interactive === "true") {
1174
1225
  view.stop();
1175
- manifest = await client.fetchManifest();
1226
+ manifest = await view.withStatus("Fetching workspace\u2026", () => client.fetchManifest());
1176
1227
  const prompted = await promptSelection(manifest);
1177
1228
  if (!prompted) {
1178
1229
  console.log("Cancelled. Nothing downloaded.");
@@ -1280,12 +1331,17 @@ async function whoamiCommand(flags) {
1280
1331
  return 1;
1281
1332
  }
1282
1333
  const client = new CutsioClient(resolveApiUrl(flags.url), apiKey);
1334
+ const view = new ProgressView();
1283
1335
  try {
1284
- console.log(JSON.stringify(await client.whoami(), null, 2));
1336
+ console.log(
1337
+ JSON.stringify(await view.withStatus("Checking key\u2026", () => client.whoami()), null, 2)
1338
+ );
1285
1339
  return 0;
1286
1340
  } catch (error) {
1287
1341
  console.error(error instanceof Error ? error.message : String(error));
1288
1342
  return 1;
1343
+ } finally {
1344
+ view.stop();
1289
1345
  }
1290
1346
  }
1291
1347
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cutsio",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Sync Cutsio mezzanine masters to a local folder",
5
5
  "homepage": "https://cutsio.com",
6
6
  "type": "module",