@officexapp/vidfarm-devcli 0.21.62 → 0.21.64

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 (28) hide show
  1. package/.agents/skills/dollarplatoon-skill/SKILL.md +163 -1174
  2. package/.agents/skills/dollarplatoon-skill/SOURCE.md +62 -0
  3. package/.agents/skills/dollarplatoon-skill/skill/clients.md +234 -0
  4. package/.agents/skills/dollarplatoon-skill/skill/feeds.md +326 -0
  5. package/.agents/skills/dollarplatoon-skill/skill/gigs.md +395 -0
  6. package/.agents/skills/dollarplatoon-skill/skill/gigworkers.md +324 -0
  7. package/.agents/skills/dollarplatoon-skill/skill/orders.md +573 -0
  8. package/.agents/skills/dollarplatoon-skill/skill/payouts.md +234 -0
  9. package/.agents/skills/dollarplatoon-skill/skill/platform.md +174 -0
  10. package/.agents/skills/dollarplatoon-skill/skill/prices.md +75 -0
  11. package/.agents/skills/dollarplatoon-skill/skill/pricing-and-tags.md +255 -0
  12. package/.agents/skills/dollarplatoon-skill/skill/proofs.md +555 -0
  13. package/.agents/skills/dollarplatoon-skill/skill/queue.md +404 -0
  14. package/.agents/skills/dollarplatoon-skill/skill/quickstart.md +191 -0
  15. package/.agents/skills/dollarplatoon-skill/skill/staging.md +178 -0
  16. package/.agents/skills/dollarplatoon-skill/skill/tasks.md +588 -0
  17. package/.agents/skills/dollarplatoon-skill/skill/web-pages.md +586 -0
  18. package/.agents/skills/vidfarm/SKILL.md +3 -3
  19. package/.agents/skills/vidfarm/references/core-workflows.md +107 -3
  20. package/SKILL.director.md +109 -5
  21. package/SKILL.md +3 -1
  22. package/clipper.md +20 -0
  23. package/dist/src/cli.js +50 -6
  24. package/dist/src/devcli/delivery-seal.js +119 -0
  25. package/dist/src/devcli/marketplace-console.js +1418 -0
  26. package/dist/src/devcli/marketplace-gigs.js +162 -16
  27. package/marketplace.md +299 -1
  28. package/package.json +24 -3
@@ -0,0 +1,1418 @@
1
+ // `vidfarm shop` and `vidfarm purchases` — the PACK MARKETPLACE from a terminal.
2
+ //
3
+ // ── what this is, and what `vidfarm gigs` is ────────────────────────────────
4
+ //
5
+ // `gigs` talks to DOLLAR PLATOON: mailboxes, tasks, proofs, payouts. It is the
6
+ // money rail underneath, and it knows nothing about shops or pack cards.
7
+ //
8
+ // THIS module talks to VIDFARM, and it is the storefront on top:
9
+ //
10
+ // vidfarm shop the VENDOR. Open a shop, put pack cards on the shelf,
11
+ // take orders, deliver them, get paid, answer reviews.
12
+ // vidfarm purchases the BUYER. Read the shelf, buy a pack, talk to the
13
+ // vendor, tear it open, rule on it, accept or refund.
14
+ //
15
+ // One person is usually both — a buyer on one order and a vendor on the next —
16
+ // which is why the two verbs share this file and one API key.
17
+ //
18
+ // EVERY ROUTE IS UNDER /api/v1/marketplace/. Until 2026-08-29 the marketplace
19
+ // was HTML forms and 302s, so none of this was reachable without a browser;
20
+ // the routes those pages now share are what this module drives.
21
+ //
22
+ // Auth: `vidfarm login`, or VIDFARM_API_KEY, or --api-key. Same key as the rest
23
+ // of the devcli. The marketplace is paid-only, so a free key answers 402.
24
+ import { parseArgs } from "node:util";
25
+ import { readFileSync } from "node:fs";
26
+ import { readStoredAuth } from "./auth-store.js";
27
+ const BOLD = "\x1b[1m";
28
+ const DIM = "\x1b[2m";
29
+ const GREEN = "\x1b[32m";
30
+ const YELLOW = "\x1b[33m";
31
+ const RED = "\x1b[31m";
32
+ const CYAN = "\x1b[1m\x1b[36m";
33
+ const RESET = "\x1b[0m";
34
+ const API = "/api/v1/marketplace";
35
+ /**
36
+ * A sentence for the person at the keyboard, never a bug.
37
+ *
38
+ * `name` is what cli.ts matches on to print the message WITHOUT a stack — a
39
+ * refused refund and a missing flag are the CLI talking, and a stack trace
40
+ * there reads as "vidfarm is broken".
41
+ */
42
+ class MarketplaceError extends Error {
43
+ name = "MarketplaceError";
44
+ }
45
+ function resolveCtx(values) {
46
+ const stored = readStoredAuth(values.home);
47
+ const host = String(values.host ?? process.env.VIDFARM_HOST ?? stored?.host ?? "https://vidfarm.cc").trim().replace(/\/+$/, "");
48
+ const apiKey = String(values["api-key"] ?? process.env.VIDFARM_API_KEY ?? stored?.apiKey ?? "").trim();
49
+ if (!apiKey) {
50
+ throw new MarketplaceError([
51
+ "No vidfarm API key.",
52
+ " Run `vidfarm login`, or export VIDFARM_API_KEY=…, or pass --api-key <key>.",
53
+ " The marketplace is paid-only — a free key reaches these routes and is refused with 402."
54
+ ].join("\n"));
55
+ }
56
+ return { host, apiKey, json: Boolean(values.json) };
57
+ }
58
+ async function call(ctx, method, path, init = {}) {
59
+ const url = new URL(path, ctx.host);
60
+ for (const [key, value] of Object.entries(init.query ?? {})) {
61
+ if (value !== undefined && value !== null && value !== "")
62
+ url.searchParams.set(key, value);
63
+ }
64
+ const headers = {
65
+ // Vidfarm authenticates on `vidfarm-api-key` ONLY. It never reads a Bearer
66
+ // token, so a Bearer-only request resolves to an anonymous caller.
67
+ "vidfarm-api-key": ctx.apiKey,
68
+ accept: "application/json"
69
+ };
70
+ if (init.body !== undefined)
71
+ headers["content-type"] = "application/json";
72
+ let response;
73
+ try {
74
+ response = await fetch(url, {
75
+ method: method.toUpperCase(),
76
+ headers,
77
+ body: init.body === undefined ? undefined : JSON.stringify(init.body)
78
+ });
79
+ }
80
+ catch (error) {
81
+ throw new MarketplaceError(`Could not reach ${ctx.host}: ${error instanceof Error ? error.message : String(error)}`);
82
+ }
83
+ const text = await response.text();
84
+ let body = null;
85
+ try {
86
+ body = text ? JSON.parse(text) : null;
87
+ }
88
+ catch {
89
+ body = null;
90
+ }
91
+ return { status: response.status, ok: response.ok, body };
92
+ }
93
+ /**
94
+ * Fail loudly, with the server's own sentence.
95
+ *
96
+ * The API answers every refusal as a sentence for a human — "This order was
97
+ * accepted and paid out. A refund is no longer possible." — so reprinting a
98
+ * bare status code would be throwing away the only useful part.
99
+ */
100
+ function assertOk(result, action) {
101
+ if (result.ok && result.body?.ok !== false)
102
+ return;
103
+ const detail = typeof result.body?.error === "string" ? ` — ${result.body.error}` : "";
104
+ const hint = result.status === 401
105
+ ? "\n Sign in: `vidfarm login`."
106
+ : result.status === 402
107
+ ? "\n The marketplace is paid-only. Upgrade at /pricing."
108
+ : result.status === 404 && typeof result.body?.create_with === "string"
109
+ ? `\n ${result.body.create_with}`
110
+ : "";
111
+ throw new MarketplaceError(`${action} failed (${result.status})${detail}.${hint}`);
112
+ }
113
+ function emit(ctx, body) {
114
+ if (!ctx.json)
115
+ return false;
116
+ process.stdout.write(`${JSON.stringify(body, null, 2)}\n`);
117
+ return true;
118
+ }
119
+ // ── formatting ──────────────────────────────────────────────────────────────
120
+ function money(value) {
121
+ const parsed = Number(value);
122
+ return Number.isFinite(parsed) ? `$${parsed.toFixed(2)}` : "$—";
123
+ }
124
+ /**
125
+ * What an order cost, in words.
126
+ *
127
+ * "$0.00" on a free order reads as a bug, and worse, it reads as a PAID order
128
+ * that went wrong — which is the one thing it must never look like, because
129
+ * the two rails have opposite undo rules. Never derive this from the number:
130
+ * a zero total reached any other way still has escrow behind it.
131
+ */
132
+ export function orderPrice(order) {
133
+ return order.is_free ? "FREE" : money(order.total_usd);
134
+ }
135
+ function shortDate(value) {
136
+ const raw = String(value ?? "");
137
+ return raw ? raw.slice(0, 10) : "—";
138
+ }
139
+ /**
140
+ * The one word that says whose move it is.
141
+ *
142
+ * Derived from the order the same way both web pages derive it, because
143
+ * `status` alone cannot answer it: a `funded` order is waiting on the VENDOR,
144
+ * a `delivered` one on the BUYER, and an order with a standing change request
145
+ * is back on the vendor whatever its status says.
146
+ */
147
+ export function orderStage(order) {
148
+ if (order.refunded_at)
149
+ return { label: order.is_free ? "cancelled" : "refunded", colour: DIM };
150
+ // A FREE order is never "paid out" — nobody was paid. It closed, and the
151
+ // buyer's files were released by the approval itself.
152
+ if (order.settled_at)
153
+ return { label: order.is_free ? "closed" : "paid out", colour: GREEN };
154
+ if (order.refund_requested_at) {
155
+ return { label: order.is_free ? "cancel asked" : "refund asked", colour: YELLOW };
156
+ }
157
+ if (order.changes_requested_at)
158
+ return { label: "changes asked", colour: YELLOW };
159
+ if (order.delivery_id)
160
+ return { label: "delivered", colour: CYAN };
161
+ // "UNFUNDED" is the vendor's do-not-work signal, and it is meaningless on the
162
+ // free rail: a free order carries no deposit BY DESIGN and is still real work
163
+ // the vendor agreed to. Flagging it red would train vendors to abandon it.
164
+ if (!order.funded && !order.is_free)
165
+ return { label: "UNFUNDED", colour: RED };
166
+ return { label: "waiting on vendor", colour: YELLOW };
167
+ }
168
+ function printOrderRow(order, opts = {}) {
169
+ const stage = orderStage(order);
170
+ const who = opts.showBuyer && (order.buyer_name || order.buyer_email)
171
+ ? ` · ${String(order.buyer_name || order.buyer_email)}`
172
+ : "";
173
+ console.log(` ${BOLD}${order.order_id}${RESET} ${order.machine_title}`
174
+ + ` ${DIM}${order.packs}× pack · ${order.video_count} videos · ${orderPrice(order)}`
175
+ + `${who} · ${shortDate(order.created_at)}${RESET}`
176
+ + ` ${stage.colour}${stage.label}${RESET}`
177
+ + (order.is_free ? ` ${CYAN}free first order${RESET}` : ""));
178
+ }
179
+ function printCardRow(card) {
180
+ const shelf = card.listed
181
+ ? `${GREEN}on the shelf${RESET}`
182
+ : card.approved
183
+ ? `${DIM}switched off${RESET}`
184
+ : `${YELLOW}awaiting review${RESET}`;
185
+ console.log(` ${BOLD}${card.title}${RESET} ${DIM}[${card.machine_id}]${RESET}`
186
+ + ` ${money(card.price_per_lot_usd)} / ${card.lot_size} videos`
187
+ + `${card.delivery_estimate ? ` ${DIM}· ${card.delivery_estimate}${RESET}` : ""} ${shelf}`);
188
+ }
189
+ // ── flag plumbing ───────────────────────────────────────────────────────────
190
+ function baseOptions() {
191
+ return {
192
+ host: { type: "string" },
193
+ "api-key": { type: "string" },
194
+ home: { type: "string" },
195
+ json: { type: "boolean", default: false }
196
+ };
197
+ }
198
+ function shopOptions() {
199
+ return {
200
+ ...baseOptions(),
201
+ name: { type: "string" },
202
+ slug: { type: "string" },
203
+ headline: { type: "string" },
204
+ bio: { type: "string" },
205
+ avatar: { type: "string" },
206
+ contact: { type: "string" },
207
+ title: { type: "string" },
208
+ note: { type: "string" },
209
+ "lot-size": { type: "string" },
210
+ price: { type: "string" },
211
+ delivery: { type: "string" },
212
+ accent: { type: "string" },
213
+ icon: { type: "string" },
214
+ caveat: { type: "string" },
215
+ require: { type: "string", multiple: true },
216
+ live: { type: "boolean" },
217
+ template: { type: "string" },
218
+ poster: { type: "string" },
219
+ video: { type: "string" },
220
+ status: { type: "string" },
221
+ machine: { type: "string" },
222
+ item: { type: "string", multiple: true },
223
+ "items-file": { type: "string" },
224
+ "items-json": { type: "string" },
225
+ "reply-to": { type: "string" },
226
+ reason: { type: "string" },
227
+ to: { type: "string" },
228
+ amount: { type: "string" },
229
+ refresh: { type: "boolean", default: false },
230
+ unseen: { type: "boolean", default: false }
231
+ };
232
+ }
233
+ function purchasesOptions() {
234
+ return {
235
+ ...baseOptions(),
236
+ shop: { type: "string" },
237
+ packs: { type: "string" },
238
+ notes: { type: "string" },
239
+ answer: { type: "string", multiple: true },
240
+ limit: { type: "string" },
241
+ "reply-to": { type: "string" },
242
+ reason: { type: "string" },
243
+ ask: { type: "boolean", default: false },
244
+ tag: { type: "string" },
245
+ note: { type: "string" },
246
+ comment: { type: "string" },
247
+ value: { type: "string" },
248
+ speed: { type: "string" },
249
+ quality: { type: "string" },
250
+ support: { type: "string" },
251
+ unseen: { type: "boolean", default: false },
252
+ yes: { type: "boolean", default: false, short: "y" }
253
+ };
254
+ }
255
+ function optionalNumber(value) {
256
+ if (value === undefined || value === null || value === "")
257
+ return undefined;
258
+ const parsed = Number(value);
259
+ if (!Number.isFinite(parsed))
260
+ throw new MarketplaceError(`"${String(value)}" is not a number.`);
261
+ return parsed;
262
+ }
263
+ function optionalText(value) {
264
+ return value === undefined ? undefined : String(value);
265
+ }
266
+ /** `--require "id=Label=hint"`, or `"Label"` on its own for a new row. */
267
+ export function parseRequirements(values) {
268
+ if (!values || !values.length)
269
+ return undefined;
270
+ return values.map((raw) => {
271
+ const parts = String(raw).split("=");
272
+ if (parts.length >= 3) {
273
+ return { id: parts[0].trim(), label: parts[1].trim(), hint: parts.slice(2).join("=").trim() };
274
+ }
275
+ if (parts.length === 2)
276
+ return { id: "", label: parts[0].trim(), hint: parts[1].trim() };
277
+ return { id: "", label: String(raw).trim(), hint: "" };
278
+ });
279
+ }
280
+ /**
281
+ * The delivered items.
282
+ *
283
+ * `--item "<preview>::<master>::<note>"`, and either URL half may be a
284
+ * `|`-separated list to make that item a slideshow. `|` is not legal in a URL
285
+ * without percent-encoding, so it can never be part of one by accident — which
286
+ * is exactly why it is the separator and a comma is not.
287
+ *
288
+ * `--items-file` / `--items-json` take the API's own shape for anything this
289
+ * cannot say.
290
+ */
291
+ export function parseDeliveryItems(values) {
292
+ const fromJson = (raw) => {
293
+ let parsed;
294
+ try {
295
+ parsed = JSON.parse(raw);
296
+ }
297
+ catch (error) {
298
+ throw new MarketplaceError(`That items JSON did not parse: ${error instanceof Error ? error.message : String(error)}`);
299
+ }
300
+ const list = Array.isArray(parsed) ? parsed : parsed?.items;
301
+ if (!Array.isArray(list))
302
+ throw new MarketplaceError("Items JSON must be a list, or { items: [...] }.");
303
+ return list.map((entry) => ({
304
+ preview_urls: [].concat(entry?.preview_urls ?? entry?.preview_url ?? []),
305
+ master_urls: [].concat(entry?.master_urls ?? entry?.master_url ?? []),
306
+ note: String(entry?.note ?? "")
307
+ }));
308
+ };
309
+ if (values["items-file"])
310
+ return fromJson(readFileSync(String(values["items-file"]), "utf8"));
311
+ if (values["items-json"])
312
+ return fromJson(String(values["items-json"]));
313
+ const items = values.item ?? [];
314
+ if (!items.length) {
315
+ throw new MarketplaceError([
316
+ "Nothing to deliver. Name the files:",
317
+ ' --item "https://…/preview.mp4"',
318
+ ' --item "https://…/preview.mp4::https://…/master.mp4::the hook lands at 0:02"',
319
+ " A slideshow is one item whose halves are |-separated lists of frame URLs.",
320
+ " Or pass the whole delivery: --items-file items.json"
321
+ ].join("\n"));
322
+ }
323
+ return items.map((raw) => {
324
+ const [preview = "", master = "", note = ""] = String(raw).split("::");
325
+ const split = (value) => value.split("|").map((url) => url.trim()).filter(Boolean);
326
+ return { preview_urls: split(preview), master_urls: split(master), note: note.trim() };
327
+ });
328
+ }
329
+ // ═══════════════════════════════════════════════════════════════════════════
330
+ // vidfarm shop — THE VENDOR
331
+ // ═══════════════════════════════════════════════════════════════════════════
332
+ export const SHOP_HELP = `vidfarm shop — your storefront on the vidfarm marketplace (VENDOR side)
333
+
334
+ A shop holds PACK CARDS. A card is one offer — "7 videos, $35, 3-5 days" — and
335
+ a buyer orders a pack from it. Every card waits for a manual review before it
336
+ reaches the public shelf; \`shop\` tells you which state each one is in.
337
+
338
+ NEW SHOP. The platform can mark a shop so that every buyer's FIRST order there
339
+ is free — one pack, $0. You cannot set it or clear it; \`shop\` reports it, and
340
+ an operator grants it. Those orders are REAL WORK with no deposit behind them,
341
+ they never pay out, and the buyer's approval is final. Work them normally.
342
+
343
+ Earning WITHOUT a shop? You do not need one — read vidfarm.cc/agentic-clipper.md
344
+ and use \`vidfarm gigs\`. A shop is how buyers come to you instead.
345
+
346
+ THE SHOP
347
+ shop Your shop, your cards, their state → GET ${API}/shop
348
+ shop open --name "<shop name>" Create it, or edit it → POST ${API}/shop/profile
349
+ --slug <address> (once, at creation) --headline "…" --bio "…"
350
+ --avatar <url> --contact <url>
351
+ shop hide | shop show Take the whole shop off the shelf / put it back
352
+ shop sync Pull gigs you own from Dollar Platoon → POST ${API}/shop/sync
353
+
354
+ THE PACK CARDS
355
+ shop cards List them
356
+ shop card <machineId> One card, with its example videos
357
+ shop new-card --title "…" --lot-size <n> --price <usd>
358
+ --delivery "3-5 days" --note "…" --caveat "before you buy…"
359
+ --accent <hue> --icon <name> --live
360
+ --require "Your brand kit=a Drive or Figma link" (repeatable)
361
+ shop edit-card <machineId> [same flags] Only what you pass changes
362
+ shop on <machineId> | shop off <machineId> Switch it on/off for buyers
363
+ shop remove-card <machineId>
364
+ shop add-example <machineId> --template <templateId> [--title --note --poster --video]
365
+ shop drop-example <machineId> <templateId>
366
+
367
+ THE ORDERS
368
+ shop orders [--status <s>] [--machine <id>] Your queue → GET ${API}/shop/orders
369
+ shop order <orderId> One order, whole, with its thread
370
+ shop say <orderId> "<message>" [--reply-to <commentId>]
371
+ shop deliver <orderId> --item "<preview>[::<master>][::<note>]" → POST …/fulfil
372
+ Repeat --item once per video. Deliver again to REVISE — what you
373
+ send replaces the pack. --items-file <file.json> for anything complex.
374
+ shop refund <orderId> [--reason "…"] You sign it; the buyer pays no gas
375
+
376
+ REVIEWS, MONEY, NEWS
377
+ shop reviews Your reviews and the summary
378
+ shop reply <reviewId> "<answer>" One answer per review, forever
379
+ shop earnings [--machine <id>] [--refresh] The payout rollup
380
+ shop withdraw --to 0x… --amount <usdc> USDC only. ETH is your gas.
381
+ shop inbox [--unseen] New orders, payouts, reviews
382
+
383
+ Every command takes --json. Auth: \`vidfarm login\` / VIDFARM_API_KEY / --api-key.`;
384
+ async function cmdShopShow(ctx) {
385
+ const result = await call(ctx, "GET", `${API}/shop`);
386
+ assertOk(result, "shop");
387
+ if (emit(ctx, result.body))
388
+ return;
389
+ const shop = result.body.shop;
390
+ if (!shop) {
391
+ console.log(`${DIM}No shop on this account yet.${RESET}`);
392
+ console.log(` Open one: ${BOLD}vidfarm shop open --name "Your Shop Name"${RESET}`);
393
+ return;
394
+ }
395
+ console.log(`${BOLD}${shop.display_name}${RESET} ${DIM}/${shop.shop_slug}${RESET}`
396
+ + (shop.hidden ? ` ${YELLOW}HIDDEN${RESET}` : ""));
397
+ if (shop.headline)
398
+ console.log(` ${shop.headline}`);
399
+ console.log(` ${CYAN}${shop.shop_url}${RESET}`);
400
+ if (shop.new_shop) {
401
+ // The vendor cannot set this and cannot clear it — it is a promise the
402
+ // PLATFORM makes to buyers, so the console reports it rather than offering
403
+ // it. Reported at all because it changes what the vendor is agreeing to:
404
+ // real work, no deposit behind it.
405
+ console.log(` ${GREEN}NEW SHOP${RESET} — every buyer's first order here is free, one pack at $0.`);
406
+ console.log(` ${DIM}Those orders carry no deposit and the buyer's approval is final.`
407
+ + ` Work them like any other order. Granted by the platform; ask an operator to change it.${RESET}`);
408
+ }
409
+ if (!result.body.dollarplatoon_connected) {
410
+ console.log(` ${YELLOW}No Dollar Platoon key on this account — your cards cannot take a paid order.${RESET}`);
411
+ console.log(` ${DIM}Connect it at ${ctx.host}/settings/marketplace${RESET}`);
412
+ }
413
+ const cards = result.body.machines ?? [];
414
+ console.log(`\n${BOLD}Pack cards${RESET} ${DIM}(${cards.length})${RESET}`);
415
+ if (!cards.length) {
416
+ console.log(` ${DIM}None yet — vidfarm shop new-card --title "…" --lot-size 7 --price 35${RESET}`);
417
+ return;
418
+ }
419
+ for (const card of cards)
420
+ printCardRow(card);
421
+ }
422
+ async function cmdShopOpen(ctx, values, hidden) {
423
+ // Read first, so `shop hide` and a headline-only edit do not blank the rest.
424
+ // The API keeps what it is not told about, but the NAME is required on every
425
+ // save — sending an empty one would be a 400 on an otherwise valid edit.
426
+ const current = await call(ctx, "GET", `${API}/shop`);
427
+ assertOk(current, "shop");
428
+ const shop = current.body.shop;
429
+ const name = optionalText(values.name) ?? shop?.display_name ?? "";
430
+ if (!name) {
431
+ throw new MarketplaceError('A new shop needs a name: vidfarm shop open --name "Your Shop Name"');
432
+ }
433
+ const body = {
434
+ display_name: name,
435
+ shop_slug: optionalText(values.slug) ?? shop?.shop_slug ?? "",
436
+ headline: optionalText(values.headline) ?? shop?.headline ?? "",
437
+ bio: optionalText(values.bio) ?? shop?.bio ?? "",
438
+ avatar_url: optionalText(values.avatar) ?? shop?.avatar_url ?? "",
439
+ contact_url: optionalText(values.contact) ?? shop?.contact_url ?? "",
440
+ hidden: hidden ?? Boolean(shop?.hidden)
441
+ };
442
+ const result = await call(ctx, "POST", `${API}/shop/profile`, { body });
443
+ assertOk(result, "shop open");
444
+ if (emit(ctx, result.body))
445
+ return;
446
+ const saved = result.body.shop;
447
+ console.log(`${GREEN}Saved.${RESET} ${BOLD}${saved.display_name}${RESET} ${DIM}/${saved.shop_slug}${RESET}`
448
+ + (saved.hidden ? ` ${YELLOW}HIDDEN${RESET}` : ""));
449
+ console.log(` ${CYAN}${saved.shop_url}${RESET}`);
450
+ }
451
+ function cardBodyFrom(values, machineId) {
452
+ return {
453
+ ...(machineId ? { machine_id: machineId } : {}),
454
+ title: optionalText(values.title) ?? "",
455
+ note: optionalText(values.note),
456
+ lot_size: optionalNumber(values["lot-size"]),
457
+ price_per_lot_usd: optionalNumber(values.price),
458
+ delivery_estimate: optionalText(values.delivery),
459
+ contact_url: optionalText(values.contact),
460
+ accent: optionalText(values.accent),
461
+ icon: optionalText(values.icon),
462
+ before_you_buy_note: optionalText(values.caveat),
463
+ requirements: parseRequirements(values.require),
464
+ // Omitted, not false. `--live` is a boolean flag, so its absence cannot be
465
+ // told apart from "off" — sending `false` would delist a card on every
466
+ // price edit. The API leaves the switch alone when the key is missing, and
467
+ // a brand-new card starts off either way.
468
+ visible: values.live === undefined ? undefined : Boolean(values.live)
469
+ };
470
+ }
471
+ async function cmdNewCard(ctx, values) {
472
+ if (!values.title)
473
+ throw new MarketplaceError('A card needs a --title, e.g. --title "UGC starter pack".');
474
+ if (values["lot-size"] === undefined || values.price === undefined) {
475
+ throw new MarketplaceError("A card needs --lot-size <videos per pack> and --price <usd per pack>.");
476
+ }
477
+ const result = await call(ctx, "POST", `${API}/shop/machines`, { body: cardBodyFrom(values) });
478
+ assertOk(result, "shop new-card");
479
+ if (emit(ctx, result.body))
480
+ return;
481
+ console.log(`${GREEN}Card created.${RESET}`);
482
+ printCardRow(result.body.machine);
483
+ if (result.body.note)
484
+ console.log(` ${YELLOW}${result.body.note}${RESET}`);
485
+ if (result.body.warning)
486
+ console.log(` ${YELLOW}${result.body.warning}${RESET}`);
487
+ }
488
+ async function cmdEditCard(ctx, values, machineId) {
489
+ if (!machineId)
490
+ throw new MarketplaceError("Which card? vidfarm shop edit-card <machineId> …");
491
+ // Read the card first, for the TITLE. Every other field the API leaves alone
492
+ // when it is not told about it, but a blank title clamps to "Untitled
493
+ // machine" — so a price-only edit would rename the card. Reading also turns
494
+ // a wrong id into a sentence instead of a 400 from the save.
495
+ const current = await call(ctx, "GET", `${API}/shop`);
496
+ assertOk(current, "shop");
497
+ const card = (current.body.machines ?? []).find((row) => row.machine_id === machineId);
498
+ if (!card)
499
+ throw new MarketplaceError(`No card "${machineId}" on your shop.`);
500
+ const body = cardBodyFrom(values, machineId);
501
+ if (!body.title)
502
+ body.title = card.title;
503
+ const result = await call(ctx, "POST", `${API}/shop/machines`, { body });
504
+ assertOk(result, "shop edit-card");
505
+ if (emit(ctx, result.body))
506
+ return;
507
+ console.log(`${GREEN}Saved.${RESET}`);
508
+ printCardRow(result.body.machine);
509
+ if (result.body.warning)
510
+ console.log(` ${YELLOW}${result.body.warning}${RESET}`);
511
+ }
512
+ async function cmdCardVisibility(ctx, machineId, visible) {
513
+ if (!machineId)
514
+ throw new MarketplaceError(`Which card? vidfarm shop ${visible ? "on" : "off"} <machineId>`);
515
+ const result = await call(ctx, "POST", `${API}/shop/machines/${encodeURIComponent(machineId)}/visibility`, { body: { visible } });
516
+ assertOk(result, "shop visibility");
517
+ if (emit(ctx, result.body))
518
+ return;
519
+ printCardRow(result.body.machine);
520
+ }
521
+ async function cmdCardDetail(ctx, machineId) {
522
+ if (!machineId)
523
+ throw new MarketplaceError("Which card? vidfarm shop card <machineId>");
524
+ const result = await call(ctx, "GET", `${API}/shop`);
525
+ assertOk(result, "shop");
526
+ const card = (result.body.machines ?? []).find((row) => row.machine_id === machineId);
527
+ if (!card)
528
+ throw new MarketplaceError(`No card "${machineId}" on your shop.`);
529
+ if (emit(ctx, card))
530
+ return;
531
+ printCardRow(card);
532
+ if (card.note)
533
+ console.log(` ${card.note}`);
534
+ if (card.before_you_buy_note)
535
+ console.log(` ${DIM}Before you buy: ${card.before_you_buy_note}${RESET}`);
536
+ if (card.requirements?.length) {
537
+ console.log(` ${BOLD}The buyer must supply${RESET}`);
538
+ for (const row of card.requirements) {
539
+ console.log(` ${row.label}${row.hint ? ` ${DIM}— ${row.hint}${RESET}` : ""} ${DIM}[${row.id}]${RESET}`);
540
+ }
541
+ }
542
+ console.log(` ${BOLD}Examples${RESET} ${DIM}(${(card.examples ?? []).length})${RESET}`);
543
+ for (const example of card.examples ?? []) {
544
+ console.log(` ${example.title || example.template_id} ${DIM}[${example.template_id}]${RESET}`);
545
+ }
546
+ console.log(` ${CYAN}${card.machine_url}${RESET}`);
547
+ }
548
+ async function cmdOrders(ctx, values) {
549
+ const result = await call(ctx, "GET", `${API}/shop/orders`, {
550
+ query: { status: optionalText(values.status), machine: optionalText(values.machine) }
551
+ });
552
+ assertOk(result, "shop orders");
553
+ if (emit(ctx, result.body))
554
+ return;
555
+ const orders = result.body.orders ?? [];
556
+ if (!orders.length) {
557
+ console.log(`${DIM}No orders yet.${RESET}`);
558
+ return;
559
+ }
560
+ console.log(`${BOLD}${orders.length} order${orders.length === 1 ? "" : "s"}${RESET}`);
561
+ for (const order of orders)
562
+ printOrderRow(order, { showBuyer: true });
563
+ console.log(`\n${DIM}Deliver one: vidfarm shop deliver <orderId> --item "<preview>::<master>"${RESET}`);
564
+ }
565
+ async function cmdOrderDetail(ctx, orderId) {
566
+ if (!orderId)
567
+ throw new MarketplaceError("Which order? vidfarm shop order <orderId>");
568
+ const result = await call(ctx, "GET", `${API}/shop/orders/${encodeURIComponent(orderId)}`);
569
+ assertOk(result, "shop order");
570
+ if (emit(ctx, result.body))
571
+ return;
572
+ const { order, buyer, delivery, thread, review } = result.body;
573
+ printOrderRow({ ...order, buyer_name: buyer?.name, buyer_email: buyer?.email }, { showBuyer: true });
574
+ if (order.is_free) {
575
+ // A vendor reading "$0" needs to know it is the promotion and not a broken
576
+ // order — and needs to know the undo rules are inverted before they ask a
577
+ // buyer to "just approve it".
578
+ console.log(` ${CYAN}This is the buyer's free first order.${RESET}`
579
+ + ` ${DIM}No deposit, no payout, and their approval closes it for good.${RESET}`);
580
+ }
581
+ if (order.notes)
582
+ console.log(`\n${BOLD}Their brief${RESET}\n ${order.notes.replace(/\n/g, "\n ")}`);
583
+ if (order.answers?.length) {
584
+ console.log(`\n${BOLD}What they supplied${RESET}`);
585
+ for (const answer of order.answers)
586
+ console.log(` ${answer.label}: ${CYAN}${answer.url}${RESET}`);
587
+ }
588
+ console.log(`\n${BOLD}Delivery${RESET}`);
589
+ if (!delivery) {
590
+ console.log(` ${YELLOW}Nothing sent yet.${RESET}`);
591
+ console.log(` ${DIM}vidfarm shop deliver ${order.order_id} --item "<preview>::<master>"${RESET}`);
592
+ }
593
+ else {
594
+ console.log(` ${delivery.items.length} item(s) · ${delivery.opened_at ? "opened" : "not opened yet"}`
595
+ + (delivery.revisions_requested ? ` · ${delivery.revisions_requested} revision(s) asked` : ""));
596
+ if (delivery.changes_requested_at) {
597
+ console.log(` ${YELLOW}Changes asked:${RESET} ${delivery.changes_requested_note ?? ""}`);
598
+ }
599
+ for (const item of delivery.items) {
600
+ const verdict = item.verdict === "approved"
601
+ ? `${GREEN}kept${RESET}`
602
+ : item.verdict === "rejected" ? `${RED}passed${RESET}` : `${DIM}not ruled on${RESET}`;
603
+ console.log(` ${item.item_id} ${verdict}${item.rejection_tag ? ` ${DIM}(${item.rejection_tag})${RESET}` : ""}`);
604
+ }
605
+ }
606
+ if (thread) {
607
+ console.log(`\n${BOLD}Thread${RESET} ${DIM}(${thread.comments.length})${RESET}`);
608
+ if (thread.error)
609
+ console.log(` ${YELLOW}${thread.error}${RESET}`);
610
+ for (const comment of thread.comments) {
611
+ console.log(` ${comment.mine ? BOLD : ""}${comment.author || comment.role}${RESET}`
612
+ + ` ${DIM}${shortDate(comment.created_at)} [${comment.id}]${RESET}`);
613
+ console.log(` ${String(comment.body).replace(/\n/g, "\n ")}`);
614
+ }
615
+ }
616
+ if (review) {
617
+ console.log(`\n${BOLD}Their review${RESET} ${review.overall}/5`);
618
+ if (review.comment)
619
+ console.log(` ${review.comment}`);
620
+ if (!review.reply)
621
+ console.log(` ${DIM}vidfarm shop reply ${review.review_id} "<your answer>"${RESET}`);
622
+ }
623
+ console.log(`\n ${CYAN}${order.vendor_url}${RESET}`);
624
+ }
625
+ async function cmdDeliver(ctx, values, orderId) {
626
+ if (!orderId)
627
+ throw new MarketplaceError("Which order? vidfarm shop deliver <orderId> --item …");
628
+ const items = parseDeliveryItems(values);
629
+ const result = await call(ctx, "POST", `${API}/shop/orders/${encodeURIComponent(orderId)}/fulfil`, { body: { items } });
630
+ assertOk(result, "shop deliver");
631
+ if (emit(ctx, result.body))
632
+ return;
633
+ console.log(`${GREEN}${result.body.message}${RESET}`);
634
+ console.log(` Pack: ${CYAN}${result.body.delivery_url}${RESET}`);
635
+ if (result.body.warning) {
636
+ // The files ARE with the buyer. Only the money rail did not hear about it,
637
+ // and running the same command again is the retry.
638
+ console.log(` ${YELLOW}${result.body.warning}${RESET}`);
639
+ console.log(` ${DIM}The buyer can see the pack. Run the same command again to retry the proof.${RESET}`);
640
+ }
641
+ }
642
+ async function cmdShopSay(ctx, values, orderId, message) {
643
+ if (!orderId || !message)
644
+ throw new MarketplaceError('vidfarm shop say <orderId> "<message>"');
645
+ const result = await call(ctx, "POST", `${API}/shop/orders/${encodeURIComponent(orderId)}/comment`, { body: { body: message, parent_id: optionalText(values["reply-to"]) ?? "" } });
646
+ assertOk(result, "shop say");
647
+ if (emit(ctx, result.body))
648
+ return;
649
+ console.log(`${GREEN}Posted.${RESET}`);
650
+ }
651
+ async function cmdShopRefund(ctx, values, orderId) {
652
+ if (!orderId)
653
+ throw new MarketplaceError("Which order? vidfarm shop refund <orderId>");
654
+ const result = await call(ctx, "POST", `${API}/shop/orders/${encodeURIComponent(orderId)}/refund`, { body: { reason: optionalText(values.reason) ?? "" } });
655
+ assertOk(result, "shop refund");
656
+ if (emit(ctx, result.body))
657
+ return;
658
+ console.log(`${GREEN}${result.body.message}${RESET}`);
659
+ if (result.body.tx_hash)
660
+ console.log(` ${DIM}tx ${result.body.tx_hash}${RESET}`);
661
+ }
662
+ async function cmdShopReviews(ctx) {
663
+ const result = await call(ctx, "GET", `${API}/shop/reviews`);
664
+ assertOk(result, "shop reviews");
665
+ if (emit(ctx, result.body))
666
+ return;
667
+ const summary = result.body.summary ?? {};
668
+ console.log(`${BOLD}${summary.overall ?? 0}/5${RESET} ${DIM}from ${summary.count ?? 0} review(s)${RESET}`);
669
+ for (const review of result.body.reviews ?? []) {
670
+ console.log(`\n ${BOLD}${review.overall}/5${RESET} ${review.author_name}`
671
+ + ` ${DIM}${shortDate(review.created_at)} [${review.review_id}]${RESET}`
672
+ + (review.verified ? ` ${GREEN}verified${RESET}` : ""));
673
+ if (review.comment)
674
+ console.log(` ${review.comment}`);
675
+ if (review.reply)
676
+ console.log(` ${DIM}You: ${review.reply}${RESET}`);
677
+ }
678
+ }
679
+ async function cmdShopReply(ctx, reviewId, reply) {
680
+ if (!reviewId || !reply)
681
+ throw new MarketplaceError('vidfarm shop reply <reviewId> "<answer>"');
682
+ const result = await call(ctx, "POST", `${API}/shop/reviews/${encodeURIComponent(reviewId)}/reply`, { body: { reply } });
683
+ assertOk(result, "shop reply");
684
+ if (emit(ctx, result.body))
685
+ return;
686
+ console.log(`${GREEN}Replied.${RESET} ${DIM}One answer per review — this cannot be edited.${RESET}`);
687
+ }
688
+ async function cmdEarnings(ctx, values) {
689
+ const result = await call(ctx, "GET", `${API}/shop/analytics`, {
690
+ query: { machine: optionalText(values.machine), refresh: values.refresh ? "1" : undefined }
691
+ });
692
+ assertOk(result, "shop earnings");
693
+ if (emit(ctx, result.body))
694
+ return;
695
+ const totals = result.body.scope?.totals ?? {};
696
+ console.log(`${BOLD}${result.body.shop_slug}${RESET} ${DIM}as of ${result.body.generated_at}`
697
+ + `${result.body.stale ? " (cached)" : ""}${RESET}`);
698
+ console.log(` Paid out ${GREEN}${money(totals.payoutsUsd)}${RESET} ${DIM}${totals.payouts ?? 0} order(s)${RESET}`);
699
+ console.log(` In escrow ${YELLOW}${money(totals.pendingUsd)}${RESET} ${DIM}${totals.pending ?? 0} order(s)${RESET}`);
700
+ console.log(` Refunded ${DIM}${money(totals.refundedUsd)} · ${totals.refunded ?? 0} order(s)${RESET}`);
701
+ console.log(` Ordered ${money(totals.ordersUsd)} ${DIM}${totals.orders ?? 0} order(s)${RESET}`);
702
+ if (result.body.shop_slug && totals.orders && !totals.ordersUsd) {
703
+ // Real orders totalling nothing is what a New Shop's early history looks
704
+ // like. Without this line it reads as a broken rollup.
705
+ console.log(` ${DIM}Orders at $0 are free first orders — they close without a payout.${RESET}`);
706
+ }
707
+ for (const machine of result.body.machines ?? []) {
708
+ console.log(` ${machine.label || machine.machineId} ${DIM}${money(machine.totals?.payoutsUsd)} paid out${RESET}`);
709
+ }
710
+ }
711
+ async function cmdWithdraw(ctx, values) {
712
+ const to = optionalText(values.to)?.trim() ?? "";
713
+ const amount = optionalNumber(values.amount);
714
+ if (!to || amount === undefined) {
715
+ throw new MarketplaceError("vidfarm shop withdraw --to 0x… --amount <usdc> (USDC only)");
716
+ }
717
+ const result = await call(ctx, "POST", `${API}/shop/withdraw`, { body: { to_address: to, amount, currency: "usdc" } });
718
+ if (!result.ok || result.body?.ok === false) {
719
+ // NEVER suggest a retry here. A transfer can reach the mempool even when it
720
+ // reports a failure, and a blind second press is how a double-send happens.
721
+ const detail = result.body?.error ? ` — ${result.body.error}` : "";
722
+ const advice = result.body?.detail ? `\n ${result.body.detail}` : "";
723
+ throw new MarketplaceError(`shop withdraw failed (${result.status})${detail}.${advice}`);
724
+ }
725
+ if (emit(ctx, result.body))
726
+ return;
727
+ console.log(`${GREEN}Sent ${money(result.body.amount)} USDC.${RESET}`);
728
+ if (result.body.tx_hash)
729
+ console.log(` ${DIM}tx ${result.body.tx_hash}${RESET}`);
730
+ }
731
+ async function cmdInbox(ctx, values) {
732
+ const result = await call(ctx, "GET", `${API}/notifications`, {
733
+ query: { filter: values.unseen ? "unseen" : undefined }
734
+ });
735
+ assertOk(result, "notifications");
736
+ if (emit(ctx, result.body))
737
+ return;
738
+ console.log(`${BOLD}${result.body.unseen}${RESET} unseen ${DIM}of ${result.body.total}${RESET}`);
739
+ for (const row of result.body.notifications ?? []) {
740
+ const seen = row.seen ? DIM : BOLD;
741
+ console.log(` ${seen}${row.title ?? row.kind ?? "(no title)"}${RESET}`
742
+ + ` ${DIM}${shortDate(row.createdAt ?? row.created_at)}${RESET}`);
743
+ if (row.body)
744
+ console.log(` ${DIM}${row.body}${RESET}`);
745
+ if (row.href || row.url)
746
+ console.log(` ${CYAN}${ctx.host}${row.href ?? row.url}${RESET}`);
747
+ }
748
+ }
749
+ export async function runShopCommand(argv) {
750
+ const sub = (argv[0] ?? "").toLowerCase();
751
+ if (["help", "--help", "-h"].includes(sub)) {
752
+ console.log(SHOP_HELP);
753
+ return;
754
+ }
755
+ const parsed = parseArgs({ args: argv, allowPositionals: true, options: shopOptions() });
756
+ const values = parsed.values;
757
+ const rest = parsed.positionals.slice(1);
758
+ const ctx = resolveCtx(values);
759
+ switch (sub) {
760
+ case "":
761
+ case "status":
762
+ case "cards":
763
+ case "machines": return cmdShopShow(ctx);
764
+ case "open":
765
+ case "profile":
766
+ case "create": return cmdShopOpen(ctx, values);
767
+ case "hide": return cmdShopOpen(ctx, values, true);
768
+ case "show":
769
+ case "unhide": return cmdShopOpen(ctx, values, false);
770
+ case "sync": {
771
+ const result = await call(ctx, "POST", `${API}/shop/sync`);
772
+ assertOk(result, "shop sync");
773
+ if (emit(ctx, result.body))
774
+ return;
775
+ console.log(`${GREEN}Synced.${RESET} ${result.body.linked} linked, ${result.body.imported} imported.`);
776
+ console.log(`${DIM}An imported machine arrives switched OFF and unapproved — importing never publishes.${RESET}`);
777
+ return;
778
+ }
779
+ case "card": return cmdCardDetail(ctx, rest[0] ?? "");
780
+ case "new-card":
781
+ case "add-card": return cmdNewCard(ctx, values);
782
+ case "edit-card":
783
+ case "edit": return cmdEditCard(ctx, values, rest[0] ?? "");
784
+ case "on": return cmdCardVisibility(ctx, rest[0] ?? "", true);
785
+ case "off": return cmdCardVisibility(ctx, rest[0] ?? "", false);
786
+ case "remove-card":
787
+ case "delete-card": {
788
+ const machineId = rest[0] ?? "";
789
+ if (!machineId)
790
+ throw new MarketplaceError("Which card? vidfarm shop remove-card <machineId>");
791
+ const result = await call(ctx, "DELETE", `${API}/shop/machines/${encodeURIComponent(machineId)}`);
792
+ assertOk(result, "shop remove-card");
793
+ if (emit(ctx, result.body))
794
+ return;
795
+ console.log(`${GREEN}Removed.${RESET}`);
796
+ return;
797
+ }
798
+ case "add-example": {
799
+ const machineId = rest[0] ?? "";
800
+ const templateId = optionalText(values.template) ?? rest[1] ?? "";
801
+ if (!machineId || !templateId) {
802
+ throw new MarketplaceError("vidfarm shop add-example <machineId> --template <templateId>");
803
+ }
804
+ const result = await call(ctx, "POST", `${API}/shop/machines/${encodeURIComponent(machineId)}/examples`, {
805
+ body: {
806
+ template_id: templateId,
807
+ title: optionalText(values.title) ?? "",
808
+ note: optionalText(values.note) ?? "",
809
+ poster_url: optionalText(values.poster) ?? "",
810
+ video_url: optionalText(values.video) ?? ""
811
+ }
812
+ });
813
+ assertOk(result, "shop add-example");
814
+ if (emit(ctx, result.body))
815
+ return;
816
+ console.log(`${GREEN}Example added.${RESET}`);
817
+ return;
818
+ }
819
+ case "drop-example":
820
+ case "remove-example": {
821
+ const machineId = rest[0] ?? "";
822
+ const templateId = rest[1] ?? optionalText(values.template) ?? "";
823
+ if (!machineId || !templateId) {
824
+ throw new MarketplaceError("vidfarm shop drop-example <machineId> <templateId>");
825
+ }
826
+ const result = await call(ctx, "DELETE", `${API}/shop/machines/${encodeURIComponent(machineId)}/examples/${encodeURIComponent(templateId)}`);
827
+ assertOk(result, "shop drop-example");
828
+ if (emit(ctx, result.body))
829
+ return;
830
+ console.log(`${GREEN}Example removed.${RESET}`);
831
+ return;
832
+ }
833
+ case "orders":
834
+ case "queue": return cmdOrders(ctx, values);
835
+ case "order": return cmdOrderDetail(ctx, rest[0] ?? "");
836
+ case "say":
837
+ case "comment":
838
+ case "reply-order": return cmdShopSay(ctx, values, rest[0] ?? "", rest.slice(1).join(" "));
839
+ case "deliver":
840
+ case "fulfil":
841
+ case "fulfill": return cmdDeliver(ctx, values, rest[0] ?? "");
842
+ case "refund": return cmdShopRefund(ctx, values, rest[0] ?? "");
843
+ case "reviews": return cmdShopReviews(ctx);
844
+ case "reply": return cmdShopReply(ctx, rest[0] ?? "", rest.slice(1).join(" "));
845
+ case "earnings":
846
+ case "analytics": return cmdEarnings(ctx, values);
847
+ case "withdraw": return cmdWithdraw(ctx, values);
848
+ case "inbox":
849
+ case "notifications": return cmdInbox(ctx, values);
850
+ default:
851
+ console.error(`Unknown shop subcommand: ${sub}\n`);
852
+ console.log(SHOP_HELP);
853
+ process.exitCode = 1;
854
+ }
855
+ }
856
+ // ═══════════════════════════════════════════════════════════════════════════
857
+ // vidfarm purchases — THE BUYER
858
+ // ═══════════════════════════════════════════════════════════════════════════
859
+ export const PURCHASES_HELP = `vidfarm purchases — buy packs of videos on the marketplace (BUYER side)
860
+
861
+ A vendor puts PACK CARDS on the shelf. You order a pack, your USDC goes into
862
+ escrow, they deliver, you rule on it, and accepting is what pays them.
863
+
864
+ AN ORDER ENDS THREE WAYS, and only one is reversible:
865
+ accept approve and pay. FINAL, in both directions.
866
+ changes free. Nothing moves — the vendor re-delivers and you rule again.
867
+ refund the escrow comes back to you. Whoever signs it pays the gas.
868
+ On a FREE order this is a cancel: it closes the order and returns
869
+ nothing, because nothing was taken.
870
+
871
+ FIRST ORDER FREE — some shops are marked NEW SHOP by the platform, and your
872
+ first order there is one pack at $0. \`purchases shelf\` flags the offer;
873
+ \`purchases pack\` says whether YOU still have yours. It is one per shop, not
874
+ one per card, and it changes the rules: no deposit, nothing to refund, and
875
+ YOUR APPROVAL IS FINAL — it releases the files at once, with no payout step
876
+ after it to hold anything back. Cancelling does not give the free order back.
877
+
878
+ THE SHELF
879
+ purchases shelf [--shop <slug>] What is for sale → GET ${API}/shelf
880
+ purchases pack <shop>/<machineId> One card, its examples, reviews, and
881
+ whether your free first order is still there
882
+
883
+ BUYING
884
+ purchases buy <shop>/<machineId> [--packs <n>] [--notes "<brief>"]
885
+ --answer <requirementId>=<url> Once per field the card demands (repeatable)
886
+ -y Skip the confirmation
887
+ → POST ${API}/purchases
888
+
889
+ YOUR ORDERS
890
+ purchases Everything you bought → GET ${API}/purchases
891
+ purchases order <orderId> One order, with its thread and files
892
+ purchases say <orderId> "<message>" [--reply-to <commentId>]
893
+ purchases files <orderId> The delivered URLs (masters after payout)
894
+
895
+ RULING ON A PACK
896
+ purchases open <orderId> Tear it — a one-time event
897
+ purchases rate <orderId> <itemId> keep|pass [--tag <why>] [--note "…"]
898
+ purchases accept <orderId> Approve and pay. IRREVERSIBLE.
899
+ purchases changes <orderId> "<what to fix>" Free. Escrow untouched.
900
+ purchases refund <orderId> [--ask] [--reason "…"]
901
+ --ask Ask the VENDOR to sign it, so it costs you no gas
902
+ purchases cancel-refund <orderId> Drop a standing ask
903
+ purchases review <orderId> --value 5 --speed 5 --quality 5 --support 5 [--comment "…"]
904
+
905
+ purchases inbox [--unseen] Deliveries, comments, refunds
906
+
907
+ Every command takes --json. Auth: \`vidfarm login\` / VIDFARM_API_KEY / --api-key.`;
908
+ /** `<shopSlug>/<machineId>`, or `--shop <slug>` plus a bare machine id. */
909
+ export function splitPackRef(ref, values) {
910
+ const raw = String(ref ?? "").trim();
911
+ const explicitShop = optionalText(values.shop)?.trim() ?? "";
912
+ if (raw.includes("/")) {
913
+ const [shop, ...machine] = raw.split("/");
914
+ return { shop: shop.trim(), machine: machine.join("/").trim() };
915
+ }
916
+ if (explicitShop && raw)
917
+ return { shop: explicitShop, machine: raw };
918
+ throw new MarketplaceError("Name the pack as <shopSlug>/<machineId> — `vidfarm purchases shelf` lists both.");
919
+ }
920
+ /**
921
+ * Does THIS account still have its free first order at this shop?
922
+ *
923
+ * A separate call on purpose. The shelf is shared-cached and can only say that
924
+ * the OFFER exists (`shop.new_shop`); the entitlement is per-account, so it has
925
+ * its own no-store route.
926
+ *
927
+ * Fails CLOSED — an unreachable answer is `false`, which prices the order at
928
+ * the paid number. Showing $0 and then charging is the one direction that is
929
+ * unforgivable, so this only ever LOWERS a price it already showed.
930
+ */
931
+ async function freeOrderAvailable(ctx, shopSlug) {
932
+ const result = await call(ctx, "GET", `${API}/free-order-status/${encodeURIComponent(shopSlug)}`)
933
+ .catch(() => null);
934
+ return result?.ok === true && result.body?.free_available === true;
935
+ }
936
+ /** The three sentences a buyer must read BEFORE they consent to a free order. */
937
+ const FREE_ORDER_TERMS = [
938
+ "It is ONE pack, at $0, and you get one per shop — not one per card.",
939
+ "Nothing is deposited, so there is no escrow and nothing to refund.",
940
+ "YOUR APPROVAL IS FINAL and releases the files at once. There is no payout to hold back,"
941
+ + " so accepting is the whole transaction."
942
+ ];
943
+ async function cmdShelf(ctx, values) {
944
+ const result = await call(ctx, "GET", `${API}/shelf`, {
945
+ query: { shop: optionalText(values.shop), limit: optionalText(values.limit) }
946
+ });
947
+ assertOk(result, "purchases shelf");
948
+ if (emit(ctx, result.body))
949
+ return;
950
+ const packs = result.body.packs ?? [];
951
+ if (!packs.length) {
952
+ console.log(`${DIM}Nothing on the shelf right now.${RESET}`);
953
+ return;
954
+ }
955
+ console.log(`${BOLD}${packs.length} pack${packs.length === 1 ? "" : "s"} for sale${RESET}`);
956
+ for (const pack of packs) {
957
+ console.log(`\n ${BOLD}${pack.title}${RESET} ${DIM}by ${pack.shop.display_name}${RESET}`
958
+ // The OFFER, which is the same for everybody and safe on a cached list.
959
+ // Whether YOU still have yours is answered by `purchases pack`.
960
+ + (pack.shop.new_shop ? ` ${GREEN}FIRST ORDER FREE${RESET}` : ""));
961
+ console.log(` ${money(pack.price_per_lot_usd)} for ${pack.lot_size} videos`
962
+ + `${pack.delivery_estimate ? ` ${DIM}· ${pack.delivery_estimate}${RESET}` : ""}`);
963
+ if (pack.note)
964
+ console.log(` ${DIM}${pack.note}${RESET}`);
965
+ console.log(` ${DIM}buy:${RESET} ${BOLD}vidfarm purchases buy ${pack.shop_slug}/${pack.machine_id}${RESET}`);
966
+ }
967
+ }
968
+ async function cmdPack(ctx, values, ref) {
969
+ const { shop, machine } = splitPackRef(ref, values);
970
+ const result = await call(ctx, "GET", `${API}/shelf/${encodeURIComponent(shop)}/${encodeURIComponent(machine)}`);
971
+ assertOk(result, "purchases pack");
972
+ if (emit(ctx, result.body))
973
+ return;
974
+ const pack = result.body.pack;
975
+ console.log(`${BOLD}${pack.title}${RESET} ${DIM}by ${result.body.shop.display_name}${RESET}`);
976
+ console.log(` ${money(pack.price_per_lot_usd)} for ${pack.lot_size} videos`
977
+ + `${pack.delivery_estimate ? ` · ${pack.delivery_estimate}` : ""}`);
978
+ if (pack.note)
979
+ console.log(` ${pack.note}`);
980
+ // The card above is a cached, public answer. THIS is the per-account one, and
981
+ // it is the only place that can say the word "you".
982
+ if (result.body.shop.new_shop) {
983
+ if (await freeOrderAvailable(ctx, result.body.shop.shop_slug)) {
984
+ console.log(`\n ${GREEN}Your first order at this shop is FREE — one pack, $0.${RESET}`);
985
+ for (const term of FREE_ORDER_TERMS)
986
+ console.log(` ${DIM}· ${term}${RESET}`);
987
+ }
988
+ else {
989
+ console.log(`\n ${DIM}This shop offers a free first order, but you have already used yours.`
990
+ + ` This one is ${money(pack.price_per_lot_usd)}.${RESET}`);
991
+ }
992
+ }
993
+ if (pack.before_you_buy_note) {
994
+ console.log(`\n ${YELLOW}Before you buy${RESET}\n ${pack.before_you_buy_note}`);
995
+ }
996
+ if (pack.requirements?.length) {
997
+ console.log(`\n ${BOLD}You must supply${RESET}`);
998
+ for (const row of pack.requirements) {
999
+ console.log(` --answer ${row.id}=<url> ${row.label}${row.hint ? ` ${DIM}(${row.hint})${RESET}` : ""}`);
1000
+ }
1001
+ }
1002
+ const summary = result.body.review_summary ?? {};
1003
+ if (summary.count)
1004
+ console.log(`\n ${BOLD}${summary.overall}/5${RESET} ${DIM}from ${summary.count} review(s)${RESET}`);
1005
+ for (const example of pack.examples ?? []) {
1006
+ console.log(` example: ${CYAN}${example.video_url ?? example.poster_url ?? example.template_id}${RESET}`);
1007
+ }
1008
+ console.log(`\n ${CYAN}${pack.machine_url}${RESET}`);
1009
+ }
1010
+ async function cmdBuy(ctx, values, ref) {
1011
+ const { shop, machine } = splitPackRef(ref, values);
1012
+ const packs = Math.max(1, Math.round(optionalNumber(values.packs) ?? 1));
1013
+ // Read the card FIRST. The price comes off the machine on the server either
1014
+ // way, so this is not about arithmetic — it is so the confirmation names the
1015
+ // real number, and so a missing `--answer` is caught before any money moves.
1016
+ const card = await call(ctx, "GET", `${API}/shelf/${encodeURIComponent(shop)}/${encodeURIComponent(machine)}`);
1017
+ assertOk(card, "purchases buy");
1018
+ const pack = card.body.pack;
1019
+ const answers = {};
1020
+ for (const raw of values.answer ?? []) {
1021
+ const index = String(raw).indexOf("=");
1022
+ if (index < 1)
1023
+ throw new MarketplaceError(`--answer must be <requirementId>=<url>, got "${raw}".`);
1024
+ answers[String(raw).slice(0, index).trim()] = String(raw).slice(index + 1).trim();
1025
+ }
1026
+ const missing = (pack.requirements ?? []).filter((row) => !answers[row.id]);
1027
+ if (missing.length) {
1028
+ throw new MarketplaceError([
1029
+ `This vendor will not start without ${missing.length} more link(s):`,
1030
+ ...missing.map((row) => ` --answer ${row.id}=<url> ${row.label}`)
1031
+ ].join("\n"));
1032
+ }
1033
+ const total = Math.round(packs * Number(pack.price_per_lot_usd) * 100) / 100;
1034
+ // ── is this one free? ──
1035
+ //
1036
+ // ADVISORY, and only ever downward. The order route claims the free pack
1037
+ // again with a conditional write, so a stale yes here cannot mint a free
1038
+ // order — it can only produce a confirmation that was too generous, and the
1039
+ // server then charges the real price. Which is why this is read BEFORE the
1040
+ // confirmation and never after: quoting $0 and then charging is the one
1041
+ // direction a buyer cannot forgive.
1042
+ const free = card.body.shop.new_shop === true
1043
+ && await freeOrderAvailable(ctx, card.body.shop.shop_slug);
1044
+ // A free order is ONE pack whatever was asked for — the free rail prices a
1045
+ // whole order at $0 and cannot discount part of one. Say so before they
1046
+ // consent, rather than silently delivering a third of what they typed.
1047
+ if (free && packs > 1 && !ctx.json) {
1048
+ console.error(`${YELLOW}Your free first order is ONE pack. `
1049
+ + `Ordering ${packs} would be charged in full — drop --packs to take the free one.${RESET}`);
1050
+ }
1051
+ if (!values.yes && !ctx.json) {
1052
+ console.log(`${BOLD}${packs} × ${pack.title}${RESET} from ${card.body.shop.display_name}`);
1053
+ if (free && packs === 1) {
1054
+ console.log(` ${pack.lot_size} videos · ${GREEN}${BOLD}FREE${RESET}`
1055
+ + ` ${DIM}— your first order at this shop${RESET}`);
1056
+ // THE CONSENT POINT. "Free" is the part a buyer reads; the part they must
1057
+ // read is that free costs them the undo. On this rail there is no escrow
1058
+ // to hold back and approval ends the order outright.
1059
+ for (const term of FREE_ORDER_TERMS)
1060
+ console.log(` ${YELLOW}· ${term}${RESET}`);
1061
+ }
1062
+ else {
1063
+ console.log(` ${packs * pack.lot_size} videos · ${BOLD}${money(total)}${RESET} into escrow now`);
1064
+ }
1065
+ if (pack.before_you_buy_note)
1066
+ console.log(` ${YELLOW}${pack.before_you_buy_note}${RESET}`);
1067
+ console.log(`\n ${DIM}Add -y to place it.${RESET}`);
1068
+ return;
1069
+ }
1070
+ const result = await call(ctx, "POST", `${API}/purchases`, {
1071
+ body: {
1072
+ shop_slug: shop,
1073
+ machine_id: machine,
1074
+ packs,
1075
+ notes: optionalText(values.notes) ?? "",
1076
+ answers,
1077
+ // One key per attempt. A retry of THIS command reuses it, so a flaky
1078
+ // connection cannot mint a second deposit for the same order.
1079
+ idempotency_key: `devcli_${shop}_${machine}_${packs}_${Date.now()}`
1080
+ }
1081
+ });
1082
+ // A funded-but-unrecorded failure still carries an order id, and that id is
1083
+ // the difference between a recoverable order and a payment nobody can name.
1084
+ if (!result.ok || result.body?.ok === false) {
1085
+ const lines = [`purchases buy failed (${result.status}) — ${result.body?.error ?? "unknown error"}.`];
1086
+ if (result.body?.order_id) {
1087
+ lines.push(` The order exists: ${result.body.order_id}`);
1088
+ lines.push(` ${result.body.funded ? "YOUR MONEY MOVED." : "No deposit was taken."}`
1089
+ + ` vidfarm purchases order ${result.body.order_id}`);
1090
+ }
1091
+ if (result.body?.retryable)
1092
+ lines.push(" This one is safe to try again.");
1093
+ throw new MarketplaceError(lines.join("\n"));
1094
+ }
1095
+ if (emit(ctx, result.body))
1096
+ return;
1097
+ console.log(`${GREEN}${result.body.replay ? "Already ordered." : "Ordered."}${RESET}`
1098
+ + ` ${BOLD}${result.body.order_id}${RESET}`);
1099
+ // THE SERVER DECIDED, not the check above. The claim is a conditional write
1100
+ // and this is its answer, so a race that lost prints the price it was charged.
1101
+ console.log(` ${result.body.video_count} videos · `
1102
+ + (result.body.free ? `${GREEN}FREE — your first order at this shop${RESET}` : money(result.body.total_usd))
1103
+ + ` · ${result.body.funded
1104
+ ? (result.body.free ? `${GREEN}live${RESET}` : `${GREEN}funded${RESET}`)
1105
+ // On the free rail there is nothing to fund, so an unfunded free order
1106
+ // means it never reached Dollar Platoon at all — a different fault, and
1107
+ // it needs a different sentence.
1108
+ : (result.body.free
1109
+ ? `${YELLOW}NOT live — it did not reach Dollar Platoon${RESET}`
1110
+ : `${YELLOW}NOT funded — no deposit was taken${RESET}`)}`);
1111
+ if (result.body.free) {
1112
+ console.log(` ${YELLOW}Your approval on this order is FINAL and releases the files at once.${RESET}`);
1113
+ }
1114
+ console.log(` ${CYAN}${ctx.host}${result.body.order_url}${RESET}`);
1115
+ }
1116
+ async function cmdPurchasesList(ctx) {
1117
+ const result = await call(ctx, "GET", `${API}/purchases`);
1118
+ assertOk(result, "purchases");
1119
+ if (emit(ctx, result.body))
1120
+ return;
1121
+ const rows = result.body.purchases ?? [];
1122
+ if (!rows.length) {
1123
+ console.log(`${DIM}Nothing bought yet — vidfarm purchases shelf${RESET}`);
1124
+ return;
1125
+ }
1126
+ for (const row of rows) {
1127
+ console.log(` ${BOLD}${row.order_id ?? row.delivery_id}${RESET} ${row.machine_title}`
1128
+ + ` ${DIM}from ${row.shop_name} · ${orderPrice(row)} · ${shortDate(row.created_at)}${RESET}`
1129
+ + ` ${row.opened_at ? `${DIM}opened${RESET}` : `${CYAN}sealed${RESET}`} ${row.status}`);
1130
+ }
1131
+ }
1132
+ async function fetchOrder(ctx, orderId) {
1133
+ if (!orderId)
1134
+ throw new MarketplaceError("Which order? Run `vidfarm purchases` to list them.");
1135
+ const result = await call(ctx, "GET", `${API}/purchases/${encodeURIComponent(orderId)}`);
1136
+ assertOk(result, "purchases order");
1137
+ return result.body;
1138
+ }
1139
+ async function cmdPurchaseDetail(ctx, orderId) {
1140
+ const body = await fetchOrder(ctx, orderId);
1141
+ if (emit(ctx, body))
1142
+ return;
1143
+ const { order, delivery, thread, review, can } = body;
1144
+ printOrderRow(order);
1145
+ console.log(` ${CYAN}${order.buyer_url}${RESET}`);
1146
+ if (delivery) {
1147
+ console.log(`\n${BOLD}The pack${RESET} ${DIM}${delivery.items.length} item(s)`
1148
+ + `${delivery.opened_at ? "" : " · still sealed"}${RESET}`);
1149
+ if (!delivery.masters_released) {
1150
+ // Two rails, two release rules, and the difference matters to a buyer
1151
+ // deciding whether to accept: on a paid order the payout unlocks the
1152
+ // masters, on a free one the approval does it by itself.
1153
+ console.log(order.is_free
1154
+ ? ` ${YELLOW}Masters are locked until you accept. On a free order accepting IS the end — there is no payout after it.${RESET}`
1155
+ : ` ${YELLOW}Masters are locked until you accept and the payout lands.${RESET}`);
1156
+ }
1157
+ for (const item of delivery.items) {
1158
+ const verdict = item.verdict === "approved"
1159
+ ? `${GREEN}kept${RESET}`
1160
+ : item.verdict === "rejected" ? `${RED}passed${RESET}` : `${DIM}not ruled on${RESET}`;
1161
+ console.log(` ${BOLD}${item.item_id}${RESET} ${verdict}${item.note ? ` ${DIM}${item.note}${RESET}` : ""}`);
1162
+ for (const url of item.preview_urls)
1163
+ console.log(` preview ${CYAN}${url}${RESET}`);
1164
+ for (const url of item.master_urls)
1165
+ console.log(` master ${CYAN}${url}${RESET}`);
1166
+ }
1167
+ if (body.zip_url)
1168
+ console.log(` ${DIM}all of it: ${body.zip_url}${RESET}`);
1169
+ }
1170
+ else {
1171
+ console.log(`\n${DIM}Nothing delivered yet.${RESET}`);
1172
+ }
1173
+ if (thread?.comments?.length) {
1174
+ console.log(`\n${BOLD}Thread${RESET}`);
1175
+ for (const comment of thread.comments) {
1176
+ console.log(` ${comment.mine ? BOLD : ""}${comment.author || comment.role}${RESET}`
1177
+ + ` ${DIM}${shortDate(comment.created_at)} [${comment.id}]${RESET}`);
1178
+ console.log(` ${String(comment.body).replace(/\n/g, "\n ")}`);
1179
+ }
1180
+ }
1181
+ if (review)
1182
+ console.log(`\n${BOLD}Your review${RESET} ${review.overall}/5`);
1183
+ const moves = [
1184
+ can?.accept
1185
+ ? (order.is_free ? "accept (FINAL, nothing charged)" : `accept (pays ${money(order.total_usd)}, FINAL)`)
1186
+ : null,
1187
+ can?.request_changes ? "changes (free)" : null,
1188
+ // "refund" on an order that cost nothing reads as a bug. Same route either
1189
+ // way — it closes the order — but the word has to match what happened.
1190
+ can?.refund ? (order.is_free ? "cancel" : "refund") : null,
1191
+ can?.review ? "review" : null
1192
+ ].filter(Boolean);
1193
+ if (moves.length)
1194
+ console.log(`\n${DIM}You can: ${moves.join(" · ")}${RESET}`);
1195
+ }
1196
+ async function cmdFiles(ctx, orderId) {
1197
+ const body = await fetchOrder(ctx, orderId);
1198
+ const items = body.delivery?.items ?? [];
1199
+ if (emit(ctx, { masters_released: Boolean(body.delivery?.masters_released), items }))
1200
+ return;
1201
+ if (!items.length) {
1202
+ console.log(`${DIM}Nothing delivered yet.${RESET}`);
1203
+ return;
1204
+ }
1205
+ for (const item of items) {
1206
+ for (const url of item.master_urls.length ? item.master_urls : item.preview_urls)
1207
+ console.log(url);
1208
+ }
1209
+ if (!body.delivery?.masters_released) {
1210
+ console.error(`${YELLOW}Watermarked previews only — the masters unlock when you accept.${RESET}`);
1211
+ }
1212
+ }
1213
+ async function cmdOpenPack(ctx, orderId) {
1214
+ const body = await fetchOrder(ctx, orderId);
1215
+ const deliveryId = body.delivery?.delivery_id;
1216
+ if (!deliveryId)
1217
+ throw new MarketplaceError("There is nothing delivered on that order yet.");
1218
+ const result = await call(ctx, "POST", `${API}/deliveries/${encodeURIComponent(deliveryId)}/open`, { body: {} });
1219
+ assertOk(result, "purchases open");
1220
+ if (emit(ctx, result.body))
1221
+ return;
1222
+ console.log(`${GREEN}Torn open.${RESET} ${body.delivery.items.length} item(s) inside.`);
1223
+ console.log(`${DIM}Rule on each: vidfarm purchases rate ${orderId} <itemId> keep|pass${RESET}`);
1224
+ }
1225
+ async function cmdRate(ctx, values, orderId, itemId, verdictWord) {
1226
+ const word = String(verdictWord ?? "").toLowerCase();
1227
+ const verdict = ["keep", "approve", "approved", "yes"].includes(word)
1228
+ ? "approved"
1229
+ : ["pass", "reject", "rejected", "no"].includes(word) ? "rejected" : "";
1230
+ if (!itemId || !verdict) {
1231
+ throw new MarketplaceError("vidfarm purchases rate <orderId> <itemId> keep|pass");
1232
+ }
1233
+ const body = await fetchOrder(ctx, orderId);
1234
+ const deliveryId = body.delivery?.delivery_id;
1235
+ if (!deliveryId)
1236
+ throw new MarketplaceError("There is nothing delivered on that order yet.");
1237
+ const result = await call(ctx, "POST", `${API}/deliveries/${encodeURIComponent(deliveryId)}/verdict`, {
1238
+ body: {
1239
+ item_id: itemId,
1240
+ verdict,
1241
+ rejection_tag: optionalText(values.tag) ?? null,
1242
+ note: optionalText(values.note) ?? null
1243
+ }
1244
+ });
1245
+ assertOk(result, "purchases rate");
1246
+ if (emit(ctx, result.body))
1247
+ return;
1248
+ console.log(`${GREEN}Recorded.${RESET} ${result.body.decided}/${result.body.total} ruled on.`);
1249
+ if (result.body.complete) {
1250
+ // Deliberately BOTH doors. A verdict is vidfarm's note of what the buyer
1251
+ // thought; accepting pays for the pack in full whatever those notes say.
1252
+ console.log(`${DIM}All of them. Now: vidfarm purchases accept ${orderId}${RESET}`);
1253
+ console.log(`${DIM} or: vidfarm purchases changes ${orderId} "<what to fix>" (free)${RESET}`);
1254
+ }
1255
+ }
1256
+ async function cmdAccept(ctx, values, orderId) {
1257
+ const body = await fetchOrder(ctx, orderId);
1258
+ const free = body.order.is_free === true;
1259
+ if (!values.yes && !ctx.json) {
1260
+ // The one command in this file that cannot be undone. It gets a
1261
+ // confirmation for the same reason the web card has a dialog — and a FREE
1262
+ // order needs it MORE, not less: nothing is charged, so the only thing the
1263
+ // buyer is spending is their right to send the pack back.
1264
+ console.log(free
1265
+ ? `${BOLD}Accepting closes this order for good. Nothing is charged — it is your free first order.${RESET}`
1266
+ : `${BOLD}Accepting pays ${money(body.order.total_usd)} to ${body.order.shop_name}, in full.${RESET}`);
1267
+ console.log(free
1268
+ ? ` Your verdict is FINAL and releases the files at once. There is no payout to hold back,`
1269
+ + ` so this IS the end of the order — you cannot ask for changes afterwards.`
1270
+ : ` Dollar Platoon's verdict is FINAL — there is no undo, and no partial payment.`);
1271
+ const rejected = (body.delivery?.items ?? []).filter((item) => item.verdict === "rejected").length;
1272
+ if (rejected) {
1273
+ console.log(free
1274
+ ? ` ${YELLOW}You marked ${rejected} video(s) wrong. Accepting takes the pack as it is.${RESET}`
1275
+ : ` ${YELLOW}You marked ${rejected} video(s) wrong. Accepting pays for them anyway.${RESET}`);
1276
+ console.log(` ${YELLOW}Free alternative: vidfarm purchases changes ${orderId} "<what to fix>"${RESET}`);
1277
+ }
1278
+ console.log(`\n ${DIM}Add -y to accept.${RESET}`);
1279
+ return;
1280
+ }
1281
+ const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/accept`, { body: {} });
1282
+ assertOk(result, "purchases accept");
1283
+ if (emit(ctx, result.body))
1284
+ return;
1285
+ console.log(`${GREEN}${result.body.message}${RESET}`);
1286
+ // Approved-but-unsettled is real and recoverable, and it is a PAID-rail
1287
+ // state only: on the free rail the approval is the whole transaction, so
1288
+ // there is no second call left to fail.
1289
+ if (!result.body.settled && !free) {
1290
+ // Never tell this buyer to accept again — there is nothing left to approve.
1291
+ console.log(`${YELLOW}The verdict landed but the payout did not. Do NOT accept again — retry the payout shortly.${RESET}`);
1292
+ }
1293
+ }
1294
+ async function cmdChanges(ctx, orderId, note) {
1295
+ if (!orderId || !note)
1296
+ throw new MarketplaceError('vidfarm purchases changes <orderId> "<what to fix>"');
1297
+ const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/request-changes`, { body: { note } });
1298
+ assertOk(result, "purchases changes");
1299
+ if (emit(ctx, result.body))
1300
+ return;
1301
+ console.log(`${GREEN}${result.body.message}${RESET}`);
1302
+ }
1303
+ async function cmdBuyerRefund(ctx, values, orderId) {
1304
+ if (!orderId)
1305
+ throw new MarketplaceError("Which order? vidfarm purchases refund <orderId>");
1306
+ const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/refund`, {
1307
+ body: { mode: values.ask ? "ask" : "self", reason: optionalText(values.reason) ?? "" }
1308
+ });
1309
+ assertOk(result, "purchases refund");
1310
+ if (emit(ctx, result.body))
1311
+ return;
1312
+ // The server's own sentence already knows which rail it was on — a free
1313
+ // order gets "cancelled", a paid one "refunded" — so it is printed as-is
1314
+ // rather than reworded here.
1315
+ console.log(`${GREEN}${result.body.message}${RESET}`);
1316
+ if (result.body.tx_hash)
1317
+ console.log(` ${DIM}tx ${result.body.tx_hash}${RESET}`);
1318
+ // A spent free order does NOT come back. Said here because this is the one
1319
+ // command that spends it, and a buyer cancelling to "re-order properly"
1320
+ // would otherwise find out by being charged.
1321
+ if (result.body.free_spent) {
1322
+ console.log(` ${YELLOW}Your free first order at this shop stays used. `
1323
+ + `A new order here will be charged.${RESET}`);
1324
+ }
1325
+ }
1326
+ async function cmdReview(ctx, values, orderId) {
1327
+ const axes = ["value", "speed", "quality", "support"];
1328
+ const scores = {};
1329
+ for (const axis of axes) {
1330
+ const score = optionalNumber(values[axis]);
1331
+ if (score === undefined) {
1332
+ throw new MarketplaceError("Score all four: --value --speed --quality --support, each 1-5."
1333
+ + "\n A missing axis would be saved as a one-star you did not write.");
1334
+ }
1335
+ scores[axis] = score;
1336
+ }
1337
+ const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/review`, {
1338
+ body: { ...scores, comment: optionalText(values.comment) ?? "" }
1339
+ });
1340
+ assertOk(result, "purchases review");
1341
+ if (emit(ctx, result.body))
1342
+ return;
1343
+ console.log(result.body.duplicate
1344
+ ? `${DIM}You already reviewed this order.${RESET}`
1345
+ : `${GREEN}Posted. ${result.body.overall}/5${RESET}`);
1346
+ }
1347
+ export async function runPurchasesCommand(argv) {
1348
+ const sub = (argv[0] ?? "").toLowerCase();
1349
+ if (["help", "--help", "-h"].includes(sub)) {
1350
+ console.log(PURCHASES_HELP);
1351
+ return;
1352
+ }
1353
+ const parsed = parseArgs({ args: argv, allowPositionals: true, options: purchasesOptions() });
1354
+ const values = parsed.values;
1355
+ const rest = parsed.positionals.slice(1);
1356
+ const ctx = resolveCtx(values);
1357
+ switch (sub) {
1358
+ case "":
1359
+ case "list":
1360
+ case "orders": return cmdPurchasesList(ctx);
1361
+ case "shelf":
1362
+ case "browse":
1363
+ case "packs": return cmdShelf(ctx, values);
1364
+ case "pack":
1365
+ case "card": return cmdPack(ctx, values, rest[0] ?? "");
1366
+ case "buy":
1367
+ case "order-pack": return cmdBuy(ctx, values, rest[0] ?? "");
1368
+ case "order":
1369
+ case "show": return cmdPurchaseDetail(ctx, rest[0] ?? "");
1370
+ case "files":
1371
+ case "download": return cmdFiles(ctx, rest[0] ?? "");
1372
+ case "say":
1373
+ case "comment": {
1374
+ const orderId = rest[0] ?? "";
1375
+ const message = rest.slice(1).join(" ");
1376
+ if (!orderId || !message)
1377
+ throw new MarketplaceError('vidfarm purchases say <orderId> "<message>"');
1378
+ const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/comment`, { body: { body: message, parent_id: optionalText(values["reply-to"]) ?? "" } });
1379
+ assertOk(result, "purchases say");
1380
+ if (emit(ctx, result.body))
1381
+ return;
1382
+ console.log(`${GREEN}Posted.${RESET}`);
1383
+ return;
1384
+ }
1385
+ case "open":
1386
+ case "tear": return cmdOpenPack(ctx, rest[0] ?? "");
1387
+ case "rate":
1388
+ case "verdict": return cmdRate(ctx, values, rest[0] ?? "", rest[1] ?? "", rest[2] ?? "");
1389
+ case "accept":
1390
+ case "approve": return cmdAccept(ctx, values, rest[0] ?? "");
1391
+ case "changes":
1392
+ case "request-changes":
1393
+ case "revise": return cmdChanges(ctx, rest[0] ?? "", rest.slice(1).join(" ") || (optionalText(values.note) ?? ""));
1394
+ case "refund":
1395
+ case "cancel": return cmdBuyerRefund(ctx, values, rest[0] ?? "");
1396
+ case "cancel-refund":
1397
+ case "drop-refund": {
1398
+ const orderId = rest[0] ?? "";
1399
+ if (!orderId)
1400
+ throw new MarketplaceError("Which order? vidfarm purchases cancel-refund <orderId>");
1401
+ const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/refund/cancel-request`, { body: {} });
1402
+ assertOk(result, "purchases cancel-refund");
1403
+ if (emit(ctx, result.body))
1404
+ return;
1405
+ console.log(`${GREEN}${result.body.message}${RESET}`);
1406
+ return;
1407
+ }
1408
+ case "review":
1409
+ case "rate-vendor": return cmdReview(ctx, values, rest[0] ?? "");
1410
+ case "inbox":
1411
+ case "notifications": return cmdInbox(ctx, values);
1412
+ default:
1413
+ console.error(`Unknown purchases subcommand: ${sub}\n`);
1414
+ console.log(PURCHASES_HELP);
1415
+ process.exitCode = 1;
1416
+ }
1417
+ }
1418
+ //# sourceMappingURL=marketplace-console.js.map