viveworker 0.8.2 → 0.8.4

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.
@@ -9,7 +9,8 @@
9
9
  * Commands:
10
10
  * viveworker share upload <file> [--password <pw>] [--price <usd> --pay-to <0x…>] [--expires-days <n>] [--no-optimize] [--json]
11
11
  * viveworker share list [--json]
12
- * viveworker share update <slug> [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>] [--json]
12
+ * viveworker share update <slug> [--file <file>] [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>] [--no-optimize] [--json]
13
+ * viveworker share replace <slug> <file> [--expires-days <n>] [--no-optimize] [--json]
13
14
  * viveworker share link <slug> --password <pw> [--ttl-hours <n>] [--json]
14
15
  * viveworker share pay <url> [--output <file>] [--dry-run] [--wallet eoa|hazbase] [--no-approval] [--json]
15
16
  * viveworker share delete <slug>
@@ -80,6 +81,8 @@ export async function runShareCli(args) {
80
81
  return handleList(args.slice(1));
81
82
  case "update":
82
83
  return handleUpdate(args.slice(1));
84
+ case "replace":
85
+ return handleReplace(args.slice(1));
83
86
  case "link":
84
87
  return handleLink(args.slice(1));
85
88
  case "pay":
@@ -99,7 +102,8 @@ function printHelp() {
99
102
  console.log("Commands:");
100
103
  console.log(" viveworker share upload <file> [--password <pw>] [--price <usd> --pay-to <0x…>] [--expires-days <n>] [--no-optimize] [--json]");
101
104
  console.log(" viveworker share list [--metrics] [--json]");
102
- console.log(" viveworker share update <slug> [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>] [--json]");
105
+ console.log(" viveworker share update <slug> [--file <file>] [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>] [--no-optimize] [--json]");
106
+ console.log(" viveworker share replace <slug> <file> [--expires-days <n>] [--no-optimize] [--json]");
103
107
  console.log(" viveworker share link <slug> --password <pw> [--ttl-hours <n>] [--json]");
104
108
  console.log(" viveworker share pay <url> [--output <file>] [--dry-run] [--no-approval] [--json]");
105
109
  console.log(" viveworker share delete <slug>");
@@ -133,28 +137,6 @@ async function handleUpload(args) {
133
137
  throw new Error("Usage: viveworker share upload <file> [--password <pw>] [--expires-days <n>]");
134
138
  }
135
139
 
136
- const absolute = path.resolve(filePath);
137
- let stat;
138
- try {
139
- stat = await fs.stat(absolute);
140
- } catch {
141
- throw new Error(`File not found: ${filePath}`);
142
- }
143
- if (!stat.isFile()) {
144
- throw new Error(`Not a regular file: ${filePath}`);
145
- }
146
- if (stat.size === 0) {
147
- throw new Error(`File is empty: ${filePath}`);
148
- }
149
- const ext = path.extname(absolute).toLowerCase();
150
- if (!ALLOWED_EXTENSIONS.includes(ext)) {
151
- throw new Error(
152
- `Accepted file types: ${ALLOWED_EXTENSIONS.join(" / ")}. ` +
153
- `Got: ${ext || "(no extension)"}`
154
- );
155
- }
156
- const { mime } = SHARE_TYPES[ext];
157
-
158
140
  const password = flags["password"] || "";
159
141
  const expiresDays = flags["expires-days"] || flags["expiresDays"] || "";
160
142
  const price = flags["price"] || "";
@@ -190,21 +172,10 @@ async function handleUpload(args) {
190
172
 
191
173
  const { apiKey, userId, shareUrl } = await resolveCredentials();
192
174
 
193
- const originalBytes = await fs.readFile(absolute);
194
- const optimization =
195
- flags["no-optimize"] || flags["noOptimize"]
196
- ? { bytes: originalBytes, info: null }
197
- : maybeOptimizeUpload({ absolute, ext, bytes: originalBytes });
198
- const bytes = optimization.bytes;
199
- if (bytes.length > MAX_FILE_SIZE) {
200
- const detail = optimization.info
201
- ? ` after optimization to ${formatSize(bytes.length)}`
202
- : "";
203
- throw new Error(`File too large (${originalBytes.length} bytes, max ${MAX_FILE_SIZE})${detail}`);
204
- }
175
+ const preparedFile = await prepareShareFile(filePath, flags);
205
176
  const form = new FormData();
206
- const blob = new Blob([bytes], { type: mime });
207
- const file = new File([blob], path.basename(absolute), { type: mime });
177
+ const blob = new Blob([preparedFile.bytes], { type: preparedFile.mime });
178
+ const file = new File([blob], path.basename(preparedFile.absolute), { type: preparedFile.mime });
208
179
  form.set("file", file);
209
180
  if (password) form.set("password", password);
210
181
  if (expiresDays) form.set("expiresDays", String(expiresDays));
@@ -233,14 +204,14 @@ async function handleUpload(args) {
233
204
  }
234
205
 
235
206
  console.log("");
236
- console.log(`✅ Uploaded ${body.originalName || path.basename(absolute)} (${formatSize(body.size)})`);
207
+ console.log(`✅ Uploaded ${body.originalName || path.basename(preparedFile.absolute)} (${formatSize(body.size)})`);
237
208
  console.log("");
238
- if (optimization.info) {
209
+ if (preparedFile.optimization.info) {
239
210
  console.log(
240
- ` Optimized HTML: ${formatSize(optimization.info.originalBytes)} → ${formatSize(optimization.info.optimizedBytes)}`
211
+ ` Optimized HTML: ${formatSize(preparedFile.optimization.info.originalBytes)} → ${formatSize(preparedFile.optimization.info.optimizedBytes)}`
241
212
  );
242
213
  console.log(
243
- ` Removed embedded fonts: ${optimization.info.removedFonts}`
214
+ ` Removed embedded fonts: ${preparedFile.optimization.info.removedFonts}`
244
215
  );
245
216
  console.log("");
246
217
  }
@@ -262,6 +233,44 @@ async function handleUpload(args) {
262
233
  console.log("");
263
234
  }
264
235
 
236
+ async function prepareShareFile(filePath, flags = {}) {
237
+ const absolute = path.resolve(String(filePath || ""));
238
+ let stat;
239
+ try {
240
+ stat = await fs.stat(absolute);
241
+ } catch {
242
+ throw new Error(`File not found: ${filePath}`);
243
+ }
244
+ if (!stat.isFile()) {
245
+ throw new Error(`Not a regular file: ${filePath}`);
246
+ }
247
+ if (stat.size === 0) {
248
+ throw new Error(`File is empty: ${filePath}`);
249
+ }
250
+ const ext = path.extname(absolute).toLowerCase();
251
+ if (!ALLOWED_EXTENSIONS.includes(ext)) {
252
+ throw new Error(
253
+ `Accepted file types: ${ALLOWED_EXTENSIONS.join(" / ")}. ` +
254
+ `Got: ${ext || "(no extension)"}`
255
+ );
256
+ }
257
+ const { mime } = SHARE_TYPES[ext];
258
+ const originalBytes = await fs.readFile(absolute);
259
+ const optimization =
260
+ flags["no-optimize"] || flags["noOptimize"]
261
+ ? { bytes: originalBytes, info: null }
262
+ : maybeOptimizeUpload({ absolute, ext, bytes: originalBytes });
263
+ const bytes = optimization.bytes;
264
+ if (bytes.length > MAX_FILE_SIZE) {
265
+ const detail = optimization.info
266
+ ? ` after optimization to ${formatSize(bytes.length)}`
267
+ : "";
268
+ throw new Error(`File too large (${originalBytes.length} bytes, max ${MAX_FILE_SIZE})${detail}`);
269
+ }
270
+
271
+ return { absolute, ext, mime, bytes, optimization };
272
+ }
273
+
265
274
  function maybeOptimizeUpload({ absolute, ext, bytes }) {
266
275
  if (ext !== ".html" && ext !== ".htm") {
267
276
  return { bytes, info: null };
@@ -506,15 +515,34 @@ function printMetricsSection(metrics, err) {
506
515
 
507
516
  async function handleUpdate(args) {
508
517
  const flags = parseFlags(args);
518
+ return handleUpdateWithFlags(flags, "update");
519
+ }
520
+
521
+ async function handleReplace(args) {
522
+ const flags = parseFlags(args);
523
+ const slug = flags._[0];
524
+ const filePath = flags._[1] || flags.file || flags.f;
525
+ if (!slug || !filePath || flags._.length > 2) {
526
+ throw new Error("Usage: viveworker share replace <slug> <file> [--expires-days <n>] [--no-optimize] [--json]");
527
+ }
528
+ flags._ = [slug];
529
+ flags.file = filePath;
530
+ return handleUpdateWithFlags(flags, "replace");
531
+ }
532
+
533
+ async function handleUpdateWithFlags(flags, mode = "update") {
509
534
  const slug = flags._[0];
510
535
  if (!slug) {
511
536
  throw new Error(
512
- "Usage: viveworker share update <slug> [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>]"
537
+ "Usage: viveworker share update <slug> [--file <file>] [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>] [--no-optimize]"
513
538
  );
514
539
  }
515
540
  if (!/^[A-Za-z0-9]+$/.test(slug)) {
516
541
  throw new Error(`Invalid slug: ${slug}`);
517
542
  }
543
+ if (flags._.length > 1) {
544
+ throw new Error("Unexpected positional argument. Use `share replace <slug> <file>` or `share update <slug> --file <file>`.");
545
+ }
518
546
 
519
547
  const hasPassword = Object.prototype.hasOwnProperty.call(flags, "password");
520
548
  const hasNoPassword = Object.prototype.hasOwnProperty.call(flags, "no-password");
@@ -525,6 +553,9 @@ async function handleUpdate(args) {
525
553
  Object.prototype.hasOwnProperty.call(flags, "noPrice");
526
554
  const hasPayTo = Object.prototype.hasOwnProperty.call(flags, "pay-to") ||
527
555
  Object.prototype.hasOwnProperty.call(flags, "payTo");
556
+ const hasFile = Object.prototype.hasOwnProperty.call(flags, "file") ||
557
+ Object.prototype.hasOwnProperty.call(flags, "f");
558
+ const filePath = flags.file || flags.f || "";
528
559
 
529
560
  if (hasPassword && hasNoPassword) {
530
561
  throw new Error("Pass either --password OR --no-password, not both");
@@ -535,9 +566,12 @@ async function handleUpdate(args) {
535
566
  if (hasPrice && hasPassword) {
536
567
  throw new Error("--price and --password cannot be combined on a single share");
537
568
  }
538
- if (!hasPassword && !hasNoPassword && !hasExpires && !hasPrice && !hasNoPrice && !hasPayTo) {
569
+ if (hasFile && (typeof filePath !== "string" || filePath.length === 0)) {
570
+ throw new Error("--file requires a path");
571
+ }
572
+ if (!hasPassword && !hasNoPassword && !hasExpires && !hasPrice && !hasNoPrice && !hasPayTo && !hasFile) {
539
573
  throw new Error(
540
- "Nothing to update — specify at least one of --password <pw>, --no-password, --price <usd>, --no-price, --pay-to <0x…>, --expires-days <n>"
574
+ "Nothing to update — specify at least one of --file <file>, --password <pw>, --no-password, --price <usd>, --no-price, --pay-to <0x…>, --expires-days <n>"
541
575
  );
542
576
  }
543
577
 
@@ -603,19 +637,44 @@ async function handleUpdate(args) {
603
637
 
604
638
  const { apiKey, userId, shareUrl } = await resolveCredentials();
605
639
 
606
- const res = await fetchWithTimeout(`${shareUrl}/api/share/${encodeURIComponent(slug)}`, {
607
- method: "PATCH",
608
- headers: {
640
+ let preparedFile = null;
641
+ let requestBody;
642
+ let requestHeaders;
643
+ let timeoutMs;
644
+ if (hasFile) {
645
+ preparedFile = await prepareShareFile(filePath, flags);
646
+ const form = new FormData();
647
+ const blob = new Blob([preparedFile.bytes], { type: preparedFile.mime });
648
+ const file = new File([blob], path.basename(preparedFile.absolute), { type: preparedFile.mime });
649
+ form.set("file", file);
650
+ for (const [key, value] of Object.entries(body)) {
651
+ form.set(key, value === null ? "" : String(value));
652
+ }
653
+ requestBody = form;
654
+ requestHeaders = {
655
+ "x-a2a-user": userId,
656
+ "x-a2a-key": apiKey,
657
+ };
658
+ timeoutMs = 60_000;
659
+ } else {
660
+ requestBody = JSON.stringify(body);
661
+ requestHeaders = {
609
662
  "content-type": "application/json",
610
663
  "x-a2a-user": userId,
611
664
  "x-a2a-key": apiKey,
612
- },
613
- body: JSON.stringify(body),
614
- }, 30_000);
665
+ };
666
+ timeoutMs = 30_000;
667
+ }
668
+
669
+ const res = await fetchWithTimeout(`${shareUrl}/api/share/${encodeURIComponent(slug)}`, {
670
+ method: "PATCH",
671
+ headers: requestHeaders,
672
+ body: requestBody,
673
+ }, timeoutMs);
615
674
 
616
675
  const respBody = await readJson(res);
617
676
  if (!res.ok || respBody.error) {
618
- throw new Error(formatApiError("Update", res.status, respBody));
677
+ throw new Error(formatApiError(mode === "replace" ? "Replace" : "Update", res.status, respBody));
619
678
  }
620
679
 
621
680
  if (flags.json) {
@@ -624,17 +683,25 @@ async function handleUpdate(args) {
624
683
  }
625
684
 
626
685
  console.log("");
627
- console.log(`✅ Updated ${slug}`);
686
+ console.log(hasFile ? `✅ Replaced file for ${slug}` : `✅ Updated ${slug}`);
628
687
  console.log("");
629
688
  console.log(` ${respBody.url}`);
630
689
  console.log("");
690
+ if (hasFile) {
691
+ console.log(` File: ${respBody.originalName || path.basename(preparedFile.absolute)} (${formatSize(respBody.size || preparedFile.bytes.length)})`);
692
+ if (preparedFile.optimization.info) {
693
+ console.log(
694
+ ` Optimized HTML: ${formatSize(preparedFile.optimization.info.originalBytes)} → ${formatSize(preparedFile.optimization.info.optimizedBytes)}`
695
+ );
696
+ }
697
+ }
631
698
  if (respBody.hasPassword) {
632
699
  console.log(` 🔒 Password-protected${hasPassword ? " (existing unlock cookies invalidated)" : ""}`);
633
700
  } else if (hasNoPassword) {
634
701
  console.log(` 🔓 Password removed`);
635
702
  }
636
703
  if (respBody.price && respBody.payTo) {
637
- const rotated = hasPrice ? " (paid sessions invalidated)" : "";
704
+ const rotated = hasPrice || respBody.paymentSessionsInvalidated ? " (paid sessions invalidated)" : "";
638
705
  console.log(
639
706
  ` 💰 Paid — ${formatUsdc(respBody.price)} USDC on ${respBody.network || "?"} → ${respBody.payTo}${rotated}`
640
707
  );
@@ -1181,6 +1248,12 @@ function formatApiError(op, status, body) {
1181
1248
  return `${op} failed (${status}): file count exceeded — ${body.current}/${body.max}. Delete something first.`;
1182
1249
  case "file-too-large":
1183
1250
  return `${op} failed (${status}): file too large (max ${formatSize(body.maxBytes)})`;
1251
+ case "empty-file":
1252
+ return `${op} failed (${status}): file is empty`;
1253
+ case "invalid-form-data":
1254
+ return `${op} failed (${status}): invalid multipart form data`;
1255
+ case "invalid-file-field":
1256
+ return `${op} failed (${status}): invalid file field`;
1184
1257
  case "unsupported-extension":
1185
1258
  return `${op} failed (${status}): unsupported file type. Accepted: ${(body.allowed || []).join(" / ") || "n/a"}`;
1186
1259
  case "unsupported-content-type":