@officexapp/vidfarm-devcli 0.21.62 → 0.21.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/dollarplatoon-skill/SKILL.md +163 -1174
- package/.agents/skills/dollarplatoon-skill/SOURCE.md +62 -0
- package/.agents/skills/dollarplatoon-skill/skill/clients.md +234 -0
- package/.agents/skills/dollarplatoon-skill/skill/feeds.md +326 -0
- package/.agents/skills/dollarplatoon-skill/skill/gigs.md +395 -0
- package/.agents/skills/dollarplatoon-skill/skill/gigworkers.md +324 -0
- package/.agents/skills/dollarplatoon-skill/skill/orders.md +573 -0
- package/.agents/skills/dollarplatoon-skill/skill/payouts.md +234 -0
- package/.agents/skills/dollarplatoon-skill/skill/platform.md +174 -0
- package/.agents/skills/dollarplatoon-skill/skill/prices.md +75 -0
- package/.agents/skills/dollarplatoon-skill/skill/pricing-and-tags.md +255 -0
- package/.agents/skills/dollarplatoon-skill/skill/proofs.md +555 -0
- package/.agents/skills/dollarplatoon-skill/skill/queue.md +404 -0
- package/.agents/skills/dollarplatoon-skill/skill/quickstart.md +191 -0
- package/.agents/skills/dollarplatoon-skill/skill/staging.md +178 -0
- package/.agents/skills/dollarplatoon-skill/skill/tasks.md +588 -0
- package/.agents/skills/dollarplatoon-skill/skill/web-pages.md +586 -0
- package/.agents/skills/vidfarm/SKILL.md +3 -3
- package/.agents/skills/vidfarm/references/core-workflows.md +39 -0
- package/SKILL.director.md +42 -3
- package/SKILL.md +3 -1
- package/clipper.md +20 -0
- package/dist/src/cli.js +50 -6
- package/dist/src/devcli/delivery-seal.js +119 -0
- package/dist/src/devcli/marketplace-console.js +1253 -0
- package/dist/src/devcli/marketplace-gigs.js +162 -16
- package/marketplace.md +275 -1
- package/package.json +24 -3
|
@@ -0,0 +1,1253 @@
|
|
|
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
|
+
function shortDate(value) {
|
|
125
|
+
const raw = String(value ?? "");
|
|
126
|
+
return raw ? raw.slice(0, 10) : "—";
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The one word that says whose move it is.
|
|
130
|
+
*
|
|
131
|
+
* Derived from the order the same way both web pages derive it, because
|
|
132
|
+
* `status` alone cannot answer it: a `funded` order is waiting on the VENDOR,
|
|
133
|
+
* a `delivered` one on the BUYER, and an order with a standing change request
|
|
134
|
+
* is back on the vendor whatever its status says.
|
|
135
|
+
*/
|
|
136
|
+
export function orderStage(order) {
|
|
137
|
+
if (order.refunded_at)
|
|
138
|
+
return { label: "refunded", colour: DIM };
|
|
139
|
+
if (order.settled_at)
|
|
140
|
+
return { label: "paid out", colour: GREEN };
|
|
141
|
+
if (order.refund_requested_at)
|
|
142
|
+
return { label: "refund asked", colour: YELLOW };
|
|
143
|
+
if (order.changes_requested_at)
|
|
144
|
+
return { label: "changes asked", colour: YELLOW };
|
|
145
|
+
if (order.delivery_id)
|
|
146
|
+
return { label: "delivered", colour: CYAN };
|
|
147
|
+
if (!order.funded)
|
|
148
|
+
return { label: "UNFUNDED", colour: RED };
|
|
149
|
+
return { label: "waiting on vendor", colour: YELLOW };
|
|
150
|
+
}
|
|
151
|
+
function printOrderRow(order, opts = {}) {
|
|
152
|
+
const stage = orderStage(order);
|
|
153
|
+
const who = opts.showBuyer && (order.buyer_name || order.buyer_email)
|
|
154
|
+
? ` · ${String(order.buyer_name || order.buyer_email)}`
|
|
155
|
+
: "";
|
|
156
|
+
console.log(` ${BOLD}${order.order_id}${RESET} ${order.machine_title}`
|
|
157
|
+
+ ` ${DIM}${order.packs}× pack · ${order.video_count} videos · ${money(order.total_usd)}`
|
|
158
|
+
+ `${who} · ${shortDate(order.created_at)}${RESET}`
|
|
159
|
+
+ ` ${stage.colour}${stage.label}${RESET}`);
|
|
160
|
+
}
|
|
161
|
+
function printCardRow(card) {
|
|
162
|
+
const shelf = card.listed
|
|
163
|
+
? `${GREEN}on the shelf${RESET}`
|
|
164
|
+
: card.approved
|
|
165
|
+
? `${DIM}switched off${RESET}`
|
|
166
|
+
: `${YELLOW}awaiting review${RESET}`;
|
|
167
|
+
console.log(` ${BOLD}${card.title}${RESET} ${DIM}[${card.machine_id}]${RESET}`
|
|
168
|
+
+ ` ${money(card.price_per_lot_usd)} / ${card.lot_size} videos`
|
|
169
|
+
+ `${card.delivery_estimate ? ` ${DIM}· ${card.delivery_estimate}${RESET}` : ""} ${shelf}`);
|
|
170
|
+
}
|
|
171
|
+
// ── flag plumbing ───────────────────────────────────────────────────────────
|
|
172
|
+
function baseOptions() {
|
|
173
|
+
return {
|
|
174
|
+
host: { type: "string" },
|
|
175
|
+
"api-key": { type: "string" },
|
|
176
|
+
home: { type: "string" },
|
|
177
|
+
json: { type: "boolean", default: false }
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function shopOptions() {
|
|
181
|
+
return {
|
|
182
|
+
...baseOptions(),
|
|
183
|
+
name: { type: "string" },
|
|
184
|
+
slug: { type: "string" },
|
|
185
|
+
headline: { type: "string" },
|
|
186
|
+
bio: { type: "string" },
|
|
187
|
+
avatar: { type: "string" },
|
|
188
|
+
contact: { type: "string" },
|
|
189
|
+
title: { type: "string" },
|
|
190
|
+
note: { type: "string" },
|
|
191
|
+
"lot-size": { type: "string" },
|
|
192
|
+
price: { type: "string" },
|
|
193
|
+
delivery: { type: "string" },
|
|
194
|
+
accent: { type: "string" },
|
|
195
|
+
icon: { type: "string" },
|
|
196
|
+
caveat: { type: "string" },
|
|
197
|
+
require: { type: "string", multiple: true },
|
|
198
|
+
live: { type: "boolean" },
|
|
199
|
+
template: { type: "string" },
|
|
200
|
+
poster: { type: "string" },
|
|
201
|
+
video: { type: "string" },
|
|
202
|
+
status: { type: "string" },
|
|
203
|
+
machine: { type: "string" },
|
|
204
|
+
item: { type: "string", multiple: true },
|
|
205
|
+
"items-file": { type: "string" },
|
|
206
|
+
"items-json": { type: "string" },
|
|
207
|
+
"reply-to": { type: "string" },
|
|
208
|
+
reason: { type: "string" },
|
|
209
|
+
to: { type: "string" },
|
|
210
|
+
amount: { type: "string" },
|
|
211
|
+
refresh: { type: "boolean", default: false },
|
|
212
|
+
unseen: { type: "boolean", default: false }
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function purchasesOptions() {
|
|
216
|
+
return {
|
|
217
|
+
...baseOptions(),
|
|
218
|
+
shop: { type: "string" },
|
|
219
|
+
packs: { type: "string" },
|
|
220
|
+
notes: { type: "string" },
|
|
221
|
+
answer: { type: "string", multiple: true },
|
|
222
|
+
limit: { type: "string" },
|
|
223
|
+
"reply-to": { type: "string" },
|
|
224
|
+
reason: { type: "string" },
|
|
225
|
+
ask: { type: "boolean", default: false },
|
|
226
|
+
tag: { type: "string" },
|
|
227
|
+
note: { type: "string" },
|
|
228
|
+
comment: { type: "string" },
|
|
229
|
+
value: { type: "string" },
|
|
230
|
+
speed: { type: "string" },
|
|
231
|
+
quality: { type: "string" },
|
|
232
|
+
support: { type: "string" },
|
|
233
|
+
unseen: { type: "boolean", default: false },
|
|
234
|
+
yes: { type: "boolean", default: false, short: "y" }
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function optionalNumber(value) {
|
|
238
|
+
if (value === undefined || value === null || value === "")
|
|
239
|
+
return undefined;
|
|
240
|
+
const parsed = Number(value);
|
|
241
|
+
if (!Number.isFinite(parsed))
|
|
242
|
+
throw new MarketplaceError(`"${String(value)}" is not a number.`);
|
|
243
|
+
return parsed;
|
|
244
|
+
}
|
|
245
|
+
function optionalText(value) {
|
|
246
|
+
return value === undefined ? undefined : String(value);
|
|
247
|
+
}
|
|
248
|
+
/** `--require "id=Label=hint"`, or `"Label"` on its own for a new row. */
|
|
249
|
+
export function parseRequirements(values) {
|
|
250
|
+
if (!values || !values.length)
|
|
251
|
+
return undefined;
|
|
252
|
+
return values.map((raw) => {
|
|
253
|
+
const parts = String(raw).split("=");
|
|
254
|
+
if (parts.length >= 3) {
|
|
255
|
+
return { id: parts[0].trim(), label: parts[1].trim(), hint: parts.slice(2).join("=").trim() };
|
|
256
|
+
}
|
|
257
|
+
if (parts.length === 2)
|
|
258
|
+
return { id: "", label: parts[0].trim(), hint: parts[1].trim() };
|
|
259
|
+
return { id: "", label: String(raw).trim(), hint: "" };
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The delivered items.
|
|
264
|
+
*
|
|
265
|
+
* `--item "<preview>::<master>::<note>"`, and either URL half may be a
|
|
266
|
+
* `|`-separated list to make that item a slideshow. `|` is not legal in a URL
|
|
267
|
+
* without percent-encoding, so it can never be part of one by accident — which
|
|
268
|
+
* is exactly why it is the separator and a comma is not.
|
|
269
|
+
*
|
|
270
|
+
* `--items-file` / `--items-json` take the API's own shape for anything this
|
|
271
|
+
* cannot say.
|
|
272
|
+
*/
|
|
273
|
+
export function parseDeliveryItems(values) {
|
|
274
|
+
const fromJson = (raw) => {
|
|
275
|
+
let parsed;
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(raw);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
throw new MarketplaceError(`That items JSON did not parse: ${error instanceof Error ? error.message : String(error)}`);
|
|
281
|
+
}
|
|
282
|
+
const list = Array.isArray(parsed) ? parsed : parsed?.items;
|
|
283
|
+
if (!Array.isArray(list))
|
|
284
|
+
throw new MarketplaceError("Items JSON must be a list, or { items: [...] }.");
|
|
285
|
+
return list.map((entry) => ({
|
|
286
|
+
preview_urls: [].concat(entry?.preview_urls ?? entry?.preview_url ?? []),
|
|
287
|
+
master_urls: [].concat(entry?.master_urls ?? entry?.master_url ?? []),
|
|
288
|
+
note: String(entry?.note ?? "")
|
|
289
|
+
}));
|
|
290
|
+
};
|
|
291
|
+
if (values["items-file"])
|
|
292
|
+
return fromJson(readFileSync(String(values["items-file"]), "utf8"));
|
|
293
|
+
if (values["items-json"])
|
|
294
|
+
return fromJson(String(values["items-json"]));
|
|
295
|
+
const items = values.item ?? [];
|
|
296
|
+
if (!items.length) {
|
|
297
|
+
throw new MarketplaceError([
|
|
298
|
+
"Nothing to deliver. Name the files:",
|
|
299
|
+
' --item "https://…/preview.mp4"',
|
|
300
|
+
' --item "https://…/preview.mp4::https://…/master.mp4::the hook lands at 0:02"',
|
|
301
|
+
" A slideshow is one item whose halves are |-separated lists of frame URLs.",
|
|
302
|
+
" Or pass the whole delivery: --items-file items.json"
|
|
303
|
+
].join("\n"));
|
|
304
|
+
}
|
|
305
|
+
return items.map((raw) => {
|
|
306
|
+
const [preview = "", master = "", note = ""] = String(raw).split("::");
|
|
307
|
+
const split = (value) => value.split("|").map((url) => url.trim()).filter(Boolean);
|
|
308
|
+
return { preview_urls: split(preview), master_urls: split(master), note: note.trim() };
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
312
|
+
// vidfarm shop — THE VENDOR
|
|
313
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
314
|
+
export const SHOP_HELP = `vidfarm shop — your storefront on the vidfarm marketplace (VENDOR side)
|
|
315
|
+
|
|
316
|
+
A shop holds PACK CARDS. A card is one offer — "7 videos, $35, 3-5 days" — and
|
|
317
|
+
a buyer orders a pack from it. Every card waits for a manual review before it
|
|
318
|
+
reaches the public shelf; \`shop\` tells you which state each one is in.
|
|
319
|
+
|
|
320
|
+
Earning WITHOUT a shop? You do not need one — read vidfarm.cc/agentic-clipper.md
|
|
321
|
+
and use \`vidfarm gigs\`. A shop is how buyers come to you instead.
|
|
322
|
+
|
|
323
|
+
THE SHOP
|
|
324
|
+
shop Your shop, your cards, their state → GET ${API}/shop
|
|
325
|
+
shop open --name "<shop name>" Create it, or edit it → POST ${API}/shop/profile
|
|
326
|
+
--slug <address> (once, at creation) --headline "…" --bio "…"
|
|
327
|
+
--avatar <url> --contact <url>
|
|
328
|
+
shop hide | shop show Take the whole shop off the shelf / put it back
|
|
329
|
+
shop sync Pull gigs you own from Dollar Platoon → POST ${API}/shop/sync
|
|
330
|
+
|
|
331
|
+
THE PACK CARDS
|
|
332
|
+
shop cards List them
|
|
333
|
+
shop card <machineId> One card, with its example videos
|
|
334
|
+
shop new-card --title "…" --lot-size <n> --price <usd>
|
|
335
|
+
--delivery "3-5 days" --note "…" --caveat "before you buy…"
|
|
336
|
+
--accent <hue> --icon <name> --live
|
|
337
|
+
--require "Your brand kit=a Drive or Figma link" (repeatable)
|
|
338
|
+
shop edit-card <machineId> [same flags] Only what you pass changes
|
|
339
|
+
shop on <machineId> | shop off <machineId> Switch it on/off for buyers
|
|
340
|
+
shop remove-card <machineId>
|
|
341
|
+
shop add-example <machineId> --template <templateId> [--title --note --poster --video]
|
|
342
|
+
shop drop-example <machineId> <templateId>
|
|
343
|
+
|
|
344
|
+
THE ORDERS
|
|
345
|
+
shop orders [--status <s>] [--machine <id>] Your queue → GET ${API}/shop/orders
|
|
346
|
+
shop order <orderId> One order, whole, with its thread
|
|
347
|
+
shop say <orderId> "<message>" [--reply-to <commentId>]
|
|
348
|
+
shop deliver <orderId> --item "<preview>[::<master>][::<note>]" → POST …/fulfil
|
|
349
|
+
Repeat --item once per video. Deliver again to REVISE — what you
|
|
350
|
+
send replaces the pack. --items-file <file.json> for anything complex.
|
|
351
|
+
shop refund <orderId> [--reason "…"] You sign it; the buyer pays no gas
|
|
352
|
+
|
|
353
|
+
REVIEWS, MONEY, NEWS
|
|
354
|
+
shop reviews Your reviews and the summary
|
|
355
|
+
shop reply <reviewId> "<answer>" One answer per review, forever
|
|
356
|
+
shop earnings [--machine <id>] [--refresh] The payout rollup
|
|
357
|
+
shop withdraw --to 0x… --amount <usdc> USDC only. ETH is your gas.
|
|
358
|
+
shop inbox [--unseen] New orders, payouts, reviews
|
|
359
|
+
|
|
360
|
+
Every command takes --json. Auth: \`vidfarm login\` / VIDFARM_API_KEY / --api-key.`;
|
|
361
|
+
async function cmdShopShow(ctx) {
|
|
362
|
+
const result = await call(ctx, "GET", `${API}/shop`);
|
|
363
|
+
assertOk(result, "shop");
|
|
364
|
+
if (emit(ctx, result.body))
|
|
365
|
+
return;
|
|
366
|
+
const shop = result.body.shop;
|
|
367
|
+
if (!shop) {
|
|
368
|
+
console.log(`${DIM}No shop on this account yet.${RESET}`);
|
|
369
|
+
console.log(` Open one: ${BOLD}vidfarm shop open --name "Your Shop Name"${RESET}`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
console.log(`${BOLD}${shop.display_name}${RESET} ${DIM}/${shop.shop_slug}${RESET}`
|
|
373
|
+
+ (shop.hidden ? ` ${YELLOW}HIDDEN${RESET}` : ""));
|
|
374
|
+
if (shop.headline)
|
|
375
|
+
console.log(` ${shop.headline}`);
|
|
376
|
+
console.log(` ${CYAN}${shop.shop_url}${RESET}`);
|
|
377
|
+
if (!result.body.dollarplatoon_connected) {
|
|
378
|
+
console.log(` ${YELLOW}No Dollar Platoon key on this account — your cards cannot take a paid order.${RESET}`);
|
|
379
|
+
console.log(` ${DIM}Connect it at ${ctx.host}/settings/marketplace${RESET}`);
|
|
380
|
+
}
|
|
381
|
+
const cards = result.body.machines ?? [];
|
|
382
|
+
console.log(`\n${BOLD}Pack cards${RESET} ${DIM}(${cards.length})${RESET}`);
|
|
383
|
+
if (!cards.length) {
|
|
384
|
+
console.log(` ${DIM}None yet — vidfarm shop new-card --title "…" --lot-size 7 --price 35${RESET}`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
for (const card of cards)
|
|
388
|
+
printCardRow(card);
|
|
389
|
+
}
|
|
390
|
+
async function cmdShopOpen(ctx, values, hidden) {
|
|
391
|
+
// Read first, so `shop hide` and a headline-only edit do not blank the rest.
|
|
392
|
+
// The API keeps what it is not told about, but the NAME is required on every
|
|
393
|
+
// save — sending an empty one would be a 400 on an otherwise valid edit.
|
|
394
|
+
const current = await call(ctx, "GET", `${API}/shop`);
|
|
395
|
+
assertOk(current, "shop");
|
|
396
|
+
const shop = current.body.shop;
|
|
397
|
+
const name = optionalText(values.name) ?? shop?.display_name ?? "";
|
|
398
|
+
if (!name) {
|
|
399
|
+
throw new MarketplaceError('A new shop needs a name: vidfarm shop open --name "Your Shop Name"');
|
|
400
|
+
}
|
|
401
|
+
const body = {
|
|
402
|
+
display_name: name,
|
|
403
|
+
shop_slug: optionalText(values.slug) ?? shop?.shop_slug ?? "",
|
|
404
|
+
headline: optionalText(values.headline) ?? shop?.headline ?? "",
|
|
405
|
+
bio: optionalText(values.bio) ?? shop?.bio ?? "",
|
|
406
|
+
avatar_url: optionalText(values.avatar) ?? shop?.avatar_url ?? "",
|
|
407
|
+
contact_url: optionalText(values.contact) ?? shop?.contact_url ?? "",
|
|
408
|
+
hidden: hidden ?? Boolean(shop?.hidden)
|
|
409
|
+
};
|
|
410
|
+
const result = await call(ctx, "POST", `${API}/shop/profile`, { body });
|
|
411
|
+
assertOk(result, "shop open");
|
|
412
|
+
if (emit(ctx, result.body))
|
|
413
|
+
return;
|
|
414
|
+
const saved = result.body.shop;
|
|
415
|
+
console.log(`${GREEN}Saved.${RESET} ${BOLD}${saved.display_name}${RESET} ${DIM}/${saved.shop_slug}${RESET}`
|
|
416
|
+
+ (saved.hidden ? ` ${YELLOW}HIDDEN${RESET}` : ""));
|
|
417
|
+
console.log(` ${CYAN}${saved.shop_url}${RESET}`);
|
|
418
|
+
}
|
|
419
|
+
function cardBodyFrom(values, machineId) {
|
|
420
|
+
return {
|
|
421
|
+
...(machineId ? { machine_id: machineId } : {}),
|
|
422
|
+
title: optionalText(values.title) ?? "",
|
|
423
|
+
note: optionalText(values.note),
|
|
424
|
+
lot_size: optionalNumber(values["lot-size"]),
|
|
425
|
+
price_per_lot_usd: optionalNumber(values.price),
|
|
426
|
+
delivery_estimate: optionalText(values.delivery),
|
|
427
|
+
contact_url: optionalText(values.contact),
|
|
428
|
+
accent: optionalText(values.accent),
|
|
429
|
+
icon: optionalText(values.icon),
|
|
430
|
+
before_you_buy_note: optionalText(values.caveat),
|
|
431
|
+
requirements: parseRequirements(values.require),
|
|
432
|
+
// Omitted, not false. `--live` is a boolean flag, so its absence cannot be
|
|
433
|
+
// told apart from "off" — sending `false` would delist a card on every
|
|
434
|
+
// price edit. The API leaves the switch alone when the key is missing, and
|
|
435
|
+
// a brand-new card starts off either way.
|
|
436
|
+
visible: values.live === undefined ? undefined : Boolean(values.live)
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
async function cmdNewCard(ctx, values) {
|
|
440
|
+
if (!values.title)
|
|
441
|
+
throw new MarketplaceError('A card needs a --title, e.g. --title "UGC starter pack".');
|
|
442
|
+
if (values["lot-size"] === undefined || values.price === undefined) {
|
|
443
|
+
throw new MarketplaceError("A card needs --lot-size <videos per pack> and --price <usd per pack>.");
|
|
444
|
+
}
|
|
445
|
+
const result = await call(ctx, "POST", `${API}/shop/machines`, { body: cardBodyFrom(values) });
|
|
446
|
+
assertOk(result, "shop new-card");
|
|
447
|
+
if (emit(ctx, result.body))
|
|
448
|
+
return;
|
|
449
|
+
console.log(`${GREEN}Card created.${RESET}`);
|
|
450
|
+
printCardRow(result.body.machine);
|
|
451
|
+
if (result.body.note)
|
|
452
|
+
console.log(` ${YELLOW}${result.body.note}${RESET}`);
|
|
453
|
+
if (result.body.warning)
|
|
454
|
+
console.log(` ${YELLOW}${result.body.warning}${RESET}`);
|
|
455
|
+
}
|
|
456
|
+
async function cmdEditCard(ctx, values, machineId) {
|
|
457
|
+
if (!machineId)
|
|
458
|
+
throw new MarketplaceError("Which card? vidfarm shop edit-card <machineId> …");
|
|
459
|
+
// Read the card first, for the TITLE. Every other field the API leaves alone
|
|
460
|
+
// when it is not told about it, but a blank title clamps to "Untitled
|
|
461
|
+
// machine" — so a price-only edit would rename the card. Reading also turns
|
|
462
|
+
// a wrong id into a sentence instead of a 400 from the save.
|
|
463
|
+
const current = await call(ctx, "GET", `${API}/shop`);
|
|
464
|
+
assertOk(current, "shop");
|
|
465
|
+
const card = (current.body.machines ?? []).find((row) => row.machine_id === machineId);
|
|
466
|
+
if (!card)
|
|
467
|
+
throw new MarketplaceError(`No card "${machineId}" on your shop.`);
|
|
468
|
+
const body = cardBodyFrom(values, machineId);
|
|
469
|
+
if (!body.title)
|
|
470
|
+
body.title = card.title;
|
|
471
|
+
const result = await call(ctx, "POST", `${API}/shop/machines`, { body });
|
|
472
|
+
assertOk(result, "shop edit-card");
|
|
473
|
+
if (emit(ctx, result.body))
|
|
474
|
+
return;
|
|
475
|
+
console.log(`${GREEN}Saved.${RESET}`);
|
|
476
|
+
printCardRow(result.body.machine);
|
|
477
|
+
if (result.body.warning)
|
|
478
|
+
console.log(` ${YELLOW}${result.body.warning}${RESET}`);
|
|
479
|
+
}
|
|
480
|
+
async function cmdCardVisibility(ctx, machineId, visible) {
|
|
481
|
+
if (!machineId)
|
|
482
|
+
throw new MarketplaceError(`Which card? vidfarm shop ${visible ? "on" : "off"} <machineId>`);
|
|
483
|
+
const result = await call(ctx, "POST", `${API}/shop/machines/${encodeURIComponent(machineId)}/visibility`, { body: { visible } });
|
|
484
|
+
assertOk(result, "shop visibility");
|
|
485
|
+
if (emit(ctx, result.body))
|
|
486
|
+
return;
|
|
487
|
+
printCardRow(result.body.machine);
|
|
488
|
+
}
|
|
489
|
+
async function cmdCardDetail(ctx, machineId) {
|
|
490
|
+
if (!machineId)
|
|
491
|
+
throw new MarketplaceError("Which card? vidfarm shop card <machineId>");
|
|
492
|
+
const result = await call(ctx, "GET", `${API}/shop`);
|
|
493
|
+
assertOk(result, "shop");
|
|
494
|
+
const card = (result.body.machines ?? []).find((row) => row.machine_id === machineId);
|
|
495
|
+
if (!card)
|
|
496
|
+
throw new MarketplaceError(`No card "${machineId}" on your shop.`);
|
|
497
|
+
if (emit(ctx, card))
|
|
498
|
+
return;
|
|
499
|
+
printCardRow(card);
|
|
500
|
+
if (card.note)
|
|
501
|
+
console.log(` ${card.note}`);
|
|
502
|
+
if (card.before_you_buy_note)
|
|
503
|
+
console.log(` ${DIM}Before you buy: ${card.before_you_buy_note}${RESET}`);
|
|
504
|
+
if (card.requirements?.length) {
|
|
505
|
+
console.log(` ${BOLD}The buyer must supply${RESET}`);
|
|
506
|
+
for (const row of card.requirements) {
|
|
507
|
+
console.log(` ${row.label}${row.hint ? ` ${DIM}— ${row.hint}${RESET}` : ""} ${DIM}[${row.id}]${RESET}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
console.log(` ${BOLD}Examples${RESET} ${DIM}(${(card.examples ?? []).length})${RESET}`);
|
|
511
|
+
for (const example of card.examples ?? []) {
|
|
512
|
+
console.log(` ${example.title || example.template_id} ${DIM}[${example.template_id}]${RESET}`);
|
|
513
|
+
}
|
|
514
|
+
console.log(` ${CYAN}${card.machine_url}${RESET}`);
|
|
515
|
+
}
|
|
516
|
+
async function cmdOrders(ctx, values) {
|
|
517
|
+
const result = await call(ctx, "GET", `${API}/shop/orders`, {
|
|
518
|
+
query: { status: optionalText(values.status), machine: optionalText(values.machine) }
|
|
519
|
+
});
|
|
520
|
+
assertOk(result, "shop orders");
|
|
521
|
+
if (emit(ctx, result.body))
|
|
522
|
+
return;
|
|
523
|
+
const orders = result.body.orders ?? [];
|
|
524
|
+
if (!orders.length) {
|
|
525
|
+
console.log(`${DIM}No orders yet.${RESET}`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
console.log(`${BOLD}${orders.length} order${orders.length === 1 ? "" : "s"}${RESET}`);
|
|
529
|
+
for (const order of orders)
|
|
530
|
+
printOrderRow(order, { showBuyer: true });
|
|
531
|
+
console.log(`\n${DIM}Deliver one: vidfarm shop deliver <orderId> --item "<preview>::<master>"${RESET}`);
|
|
532
|
+
}
|
|
533
|
+
async function cmdOrderDetail(ctx, orderId) {
|
|
534
|
+
if (!orderId)
|
|
535
|
+
throw new MarketplaceError("Which order? vidfarm shop order <orderId>");
|
|
536
|
+
const result = await call(ctx, "GET", `${API}/shop/orders/${encodeURIComponent(orderId)}`);
|
|
537
|
+
assertOk(result, "shop order");
|
|
538
|
+
if (emit(ctx, result.body))
|
|
539
|
+
return;
|
|
540
|
+
const { order, buyer, delivery, thread, review } = result.body;
|
|
541
|
+
printOrderRow({ ...order, buyer_name: buyer?.name, buyer_email: buyer?.email }, { showBuyer: true });
|
|
542
|
+
if (order.notes)
|
|
543
|
+
console.log(`\n${BOLD}Their brief${RESET}\n ${order.notes.replace(/\n/g, "\n ")}`);
|
|
544
|
+
if (order.answers?.length) {
|
|
545
|
+
console.log(`\n${BOLD}What they supplied${RESET}`);
|
|
546
|
+
for (const answer of order.answers)
|
|
547
|
+
console.log(` ${answer.label}: ${CYAN}${answer.url}${RESET}`);
|
|
548
|
+
}
|
|
549
|
+
console.log(`\n${BOLD}Delivery${RESET}`);
|
|
550
|
+
if (!delivery) {
|
|
551
|
+
console.log(` ${YELLOW}Nothing sent yet.${RESET}`);
|
|
552
|
+
console.log(` ${DIM}vidfarm shop deliver ${order.order_id} --item "<preview>::<master>"${RESET}`);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
console.log(` ${delivery.items.length} item(s) · ${delivery.opened_at ? "opened" : "not opened yet"}`
|
|
556
|
+
+ (delivery.revisions_requested ? ` · ${delivery.revisions_requested} revision(s) asked` : ""));
|
|
557
|
+
if (delivery.changes_requested_at) {
|
|
558
|
+
console.log(` ${YELLOW}Changes asked:${RESET} ${delivery.changes_requested_note ?? ""}`);
|
|
559
|
+
}
|
|
560
|
+
for (const item of delivery.items) {
|
|
561
|
+
const verdict = item.verdict === "approved"
|
|
562
|
+
? `${GREEN}kept${RESET}`
|
|
563
|
+
: item.verdict === "rejected" ? `${RED}passed${RESET}` : `${DIM}not ruled on${RESET}`;
|
|
564
|
+
console.log(` ${item.item_id} ${verdict}${item.rejection_tag ? ` ${DIM}(${item.rejection_tag})${RESET}` : ""}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (thread) {
|
|
568
|
+
console.log(`\n${BOLD}Thread${RESET} ${DIM}(${thread.comments.length})${RESET}`);
|
|
569
|
+
if (thread.error)
|
|
570
|
+
console.log(` ${YELLOW}${thread.error}${RESET}`);
|
|
571
|
+
for (const comment of thread.comments) {
|
|
572
|
+
console.log(` ${comment.mine ? BOLD : ""}${comment.author || comment.role}${RESET}`
|
|
573
|
+
+ ` ${DIM}${shortDate(comment.created_at)} [${comment.id}]${RESET}`);
|
|
574
|
+
console.log(` ${String(comment.body).replace(/\n/g, "\n ")}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (review) {
|
|
578
|
+
console.log(`\n${BOLD}Their review${RESET} ${review.overall}/5`);
|
|
579
|
+
if (review.comment)
|
|
580
|
+
console.log(` ${review.comment}`);
|
|
581
|
+
if (!review.reply)
|
|
582
|
+
console.log(` ${DIM}vidfarm shop reply ${review.review_id} "<your answer>"${RESET}`);
|
|
583
|
+
}
|
|
584
|
+
console.log(`\n ${CYAN}${order.vendor_url}${RESET}`);
|
|
585
|
+
}
|
|
586
|
+
async function cmdDeliver(ctx, values, orderId) {
|
|
587
|
+
if (!orderId)
|
|
588
|
+
throw new MarketplaceError("Which order? vidfarm shop deliver <orderId> --item …");
|
|
589
|
+
const items = parseDeliveryItems(values);
|
|
590
|
+
const result = await call(ctx, "POST", `${API}/shop/orders/${encodeURIComponent(orderId)}/fulfil`, { body: { items } });
|
|
591
|
+
assertOk(result, "shop deliver");
|
|
592
|
+
if (emit(ctx, result.body))
|
|
593
|
+
return;
|
|
594
|
+
console.log(`${GREEN}${result.body.message}${RESET}`);
|
|
595
|
+
console.log(` Pack: ${CYAN}${result.body.delivery_url}${RESET}`);
|
|
596
|
+
if (result.body.warning) {
|
|
597
|
+
// The files ARE with the buyer. Only the money rail did not hear about it,
|
|
598
|
+
// and running the same command again is the retry.
|
|
599
|
+
console.log(` ${YELLOW}${result.body.warning}${RESET}`);
|
|
600
|
+
console.log(` ${DIM}The buyer can see the pack. Run the same command again to retry the proof.${RESET}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async function cmdShopSay(ctx, values, orderId, message) {
|
|
604
|
+
if (!orderId || !message)
|
|
605
|
+
throw new MarketplaceError('vidfarm shop say <orderId> "<message>"');
|
|
606
|
+
const result = await call(ctx, "POST", `${API}/shop/orders/${encodeURIComponent(orderId)}/comment`, { body: { body: message, parent_id: optionalText(values["reply-to"]) ?? "" } });
|
|
607
|
+
assertOk(result, "shop say");
|
|
608
|
+
if (emit(ctx, result.body))
|
|
609
|
+
return;
|
|
610
|
+
console.log(`${GREEN}Posted.${RESET}`);
|
|
611
|
+
}
|
|
612
|
+
async function cmdShopRefund(ctx, values, orderId) {
|
|
613
|
+
if (!orderId)
|
|
614
|
+
throw new MarketplaceError("Which order? vidfarm shop refund <orderId>");
|
|
615
|
+
const result = await call(ctx, "POST", `${API}/shop/orders/${encodeURIComponent(orderId)}/refund`, { body: { reason: optionalText(values.reason) ?? "" } });
|
|
616
|
+
assertOk(result, "shop refund");
|
|
617
|
+
if (emit(ctx, result.body))
|
|
618
|
+
return;
|
|
619
|
+
console.log(`${GREEN}${result.body.message}${RESET}`);
|
|
620
|
+
if (result.body.tx_hash)
|
|
621
|
+
console.log(` ${DIM}tx ${result.body.tx_hash}${RESET}`);
|
|
622
|
+
}
|
|
623
|
+
async function cmdShopReviews(ctx) {
|
|
624
|
+
const result = await call(ctx, "GET", `${API}/shop/reviews`);
|
|
625
|
+
assertOk(result, "shop reviews");
|
|
626
|
+
if (emit(ctx, result.body))
|
|
627
|
+
return;
|
|
628
|
+
const summary = result.body.summary ?? {};
|
|
629
|
+
console.log(`${BOLD}${summary.overall ?? 0}/5${RESET} ${DIM}from ${summary.count ?? 0} review(s)${RESET}`);
|
|
630
|
+
for (const review of result.body.reviews ?? []) {
|
|
631
|
+
console.log(`\n ${BOLD}${review.overall}/5${RESET} ${review.author_name}`
|
|
632
|
+
+ ` ${DIM}${shortDate(review.created_at)} [${review.review_id}]${RESET}`
|
|
633
|
+
+ (review.verified ? ` ${GREEN}verified${RESET}` : ""));
|
|
634
|
+
if (review.comment)
|
|
635
|
+
console.log(` ${review.comment}`);
|
|
636
|
+
if (review.reply)
|
|
637
|
+
console.log(` ${DIM}You: ${review.reply}${RESET}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function cmdShopReply(ctx, reviewId, reply) {
|
|
641
|
+
if (!reviewId || !reply)
|
|
642
|
+
throw new MarketplaceError('vidfarm shop reply <reviewId> "<answer>"');
|
|
643
|
+
const result = await call(ctx, "POST", `${API}/shop/reviews/${encodeURIComponent(reviewId)}/reply`, { body: { reply } });
|
|
644
|
+
assertOk(result, "shop reply");
|
|
645
|
+
if (emit(ctx, result.body))
|
|
646
|
+
return;
|
|
647
|
+
console.log(`${GREEN}Replied.${RESET} ${DIM}One answer per review — this cannot be edited.${RESET}`);
|
|
648
|
+
}
|
|
649
|
+
async function cmdEarnings(ctx, values) {
|
|
650
|
+
const result = await call(ctx, "GET", `${API}/shop/analytics`, {
|
|
651
|
+
query: { machine: optionalText(values.machine), refresh: values.refresh ? "1" : undefined }
|
|
652
|
+
});
|
|
653
|
+
assertOk(result, "shop earnings");
|
|
654
|
+
if (emit(ctx, result.body))
|
|
655
|
+
return;
|
|
656
|
+
const totals = result.body.scope?.totals ?? {};
|
|
657
|
+
console.log(`${BOLD}${result.body.shop_slug}${RESET} ${DIM}as of ${result.body.generated_at}`
|
|
658
|
+
+ `${result.body.stale ? " (cached)" : ""}${RESET}`);
|
|
659
|
+
console.log(` Paid out ${GREEN}${money(totals.payoutsUsd)}${RESET} ${DIM}${totals.payouts ?? 0} order(s)${RESET}`);
|
|
660
|
+
console.log(` In escrow ${YELLOW}${money(totals.pendingUsd)}${RESET} ${DIM}${totals.pending ?? 0} order(s)${RESET}`);
|
|
661
|
+
console.log(` Refunded ${DIM}${money(totals.refundedUsd)} · ${totals.refunded ?? 0} order(s)${RESET}`);
|
|
662
|
+
console.log(` Ordered ${money(totals.ordersUsd)} ${DIM}${totals.orders ?? 0} order(s)${RESET}`);
|
|
663
|
+
for (const machine of result.body.machines ?? []) {
|
|
664
|
+
console.log(` ${machine.label || machine.machineId} ${DIM}${money(machine.totals?.payoutsUsd)} paid out${RESET}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async function cmdWithdraw(ctx, values) {
|
|
668
|
+
const to = optionalText(values.to)?.trim() ?? "";
|
|
669
|
+
const amount = optionalNumber(values.amount);
|
|
670
|
+
if (!to || amount === undefined) {
|
|
671
|
+
throw new MarketplaceError("vidfarm shop withdraw --to 0x… --amount <usdc> (USDC only)");
|
|
672
|
+
}
|
|
673
|
+
const result = await call(ctx, "POST", `${API}/shop/withdraw`, { body: { to_address: to, amount, currency: "usdc" } });
|
|
674
|
+
if (!result.ok || result.body?.ok === false) {
|
|
675
|
+
// NEVER suggest a retry here. A transfer can reach the mempool even when it
|
|
676
|
+
// reports a failure, and a blind second press is how a double-send happens.
|
|
677
|
+
const detail = result.body?.error ? ` — ${result.body.error}` : "";
|
|
678
|
+
const advice = result.body?.detail ? `\n ${result.body.detail}` : "";
|
|
679
|
+
throw new MarketplaceError(`shop withdraw failed (${result.status})${detail}.${advice}`);
|
|
680
|
+
}
|
|
681
|
+
if (emit(ctx, result.body))
|
|
682
|
+
return;
|
|
683
|
+
console.log(`${GREEN}Sent ${money(result.body.amount)} USDC.${RESET}`);
|
|
684
|
+
if (result.body.tx_hash)
|
|
685
|
+
console.log(` ${DIM}tx ${result.body.tx_hash}${RESET}`);
|
|
686
|
+
}
|
|
687
|
+
async function cmdInbox(ctx, values) {
|
|
688
|
+
const result = await call(ctx, "GET", `${API}/notifications`, {
|
|
689
|
+
query: { filter: values.unseen ? "unseen" : undefined }
|
|
690
|
+
});
|
|
691
|
+
assertOk(result, "notifications");
|
|
692
|
+
if (emit(ctx, result.body))
|
|
693
|
+
return;
|
|
694
|
+
console.log(`${BOLD}${result.body.unseen}${RESET} unseen ${DIM}of ${result.body.total}${RESET}`);
|
|
695
|
+
for (const row of result.body.notifications ?? []) {
|
|
696
|
+
const seen = row.seen ? DIM : BOLD;
|
|
697
|
+
console.log(` ${seen}${row.title ?? row.kind ?? "(no title)"}${RESET}`
|
|
698
|
+
+ ` ${DIM}${shortDate(row.createdAt ?? row.created_at)}${RESET}`);
|
|
699
|
+
if (row.body)
|
|
700
|
+
console.log(` ${DIM}${row.body}${RESET}`);
|
|
701
|
+
if (row.href || row.url)
|
|
702
|
+
console.log(` ${CYAN}${ctx.host}${row.href ?? row.url}${RESET}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
export async function runShopCommand(argv) {
|
|
706
|
+
const sub = (argv[0] ?? "").toLowerCase();
|
|
707
|
+
if (["help", "--help", "-h"].includes(sub)) {
|
|
708
|
+
console.log(SHOP_HELP);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: shopOptions() });
|
|
712
|
+
const values = parsed.values;
|
|
713
|
+
const rest = parsed.positionals.slice(1);
|
|
714
|
+
const ctx = resolveCtx(values);
|
|
715
|
+
switch (sub) {
|
|
716
|
+
case "":
|
|
717
|
+
case "status":
|
|
718
|
+
case "cards":
|
|
719
|
+
case "machines": return cmdShopShow(ctx);
|
|
720
|
+
case "open":
|
|
721
|
+
case "profile":
|
|
722
|
+
case "create": return cmdShopOpen(ctx, values);
|
|
723
|
+
case "hide": return cmdShopOpen(ctx, values, true);
|
|
724
|
+
case "show":
|
|
725
|
+
case "unhide": return cmdShopOpen(ctx, values, false);
|
|
726
|
+
case "sync": {
|
|
727
|
+
const result = await call(ctx, "POST", `${API}/shop/sync`);
|
|
728
|
+
assertOk(result, "shop sync");
|
|
729
|
+
if (emit(ctx, result.body))
|
|
730
|
+
return;
|
|
731
|
+
console.log(`${GREEN}Synced.${RESET} ${result.body.linked} linked, ${result.body.imported} imported.`);
|
|
732
|
+
console.log(`${DIM}An imported machine arrives switched OFF and unapproved — importing never publishes.${RESET}`);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
case "card": return cmdCardDetail(ctx, rest[0] ?? "");
|
|
736
|
+
case "new-card":
|
|
737
|
+
case "add-card": return cmdNewCard(ctx, values);
|
|
738
|
+
case "edit-card":
|
|
739
|
+
case "edit": return cmdEditCard(ctx, values, rest[0] ?? "");
|
|
740
|
+
case "on": return cmdCardVisibility(ctx, rest[0] ?? "", true);
|
|
741
|
+
case "off": return cmdCardVisibility(ctx, rest[0] ?? "", false);
|
|
742
|
+
case "remove-card":
|
|
743
|
+
case "delete-card": {
|
|
744
|
+
const machineId = rest[0] ?? "";
|
|
745
|
+
if (!machineId)
|
|
746
|
+
throw new MarketplaceError("Which card? vidfarm shop remove-card <machineId>");
|
|
747
|
+
const result = await call(ctx, "DELETE", `${API}/shop/machines/${encodeURIComponent(machineId)}`);
|
|
748
|
+
assertOk(result, "shop remove-card");
|
|
749
|
+
if (emit(ctx, result.body))
|
|
750
|
+
return;
|
|
751
|
+
console.log(`${GREEN}Removed.${RESET}`);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
case "add-example": {
|
|
755
|
+
const machineId = rest[0] ?? "";
|
|
756
|
+
const templateId = optionalText(values.template) ?? rest[1] ?? "";
|
|
757
|
+
if (!machineId || !templateId) {
|
|
758
|
+
throw new MarketplaceError("vidfarm shop add-example <machineId> --template <templateId>");
|
|
759
|
+
}
|
|
760
|
+
const result = await call(ctx, "POST", `${API}/shop/machines/${encodeURIComponent(machineId)}/examples`, {
|
|
761
|
+
body: {
|
|
762
|
+
template_id: templateId,
|
|
763
|
+
title: optionalText(values.title) ?? "",
|
|
764
|
+
note: optionalText(values.note) ?? "",
|
|
765
|
+
poster_url: optionalText(values.poster) ?? "",
|
|
766
|
+
video_url: optionalText(values.video) ?? ""
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
assertOk(result, "shop add-example");
|
|
770
|
+
if (emit(ctx, result.body))
|
|
771
|
+
return;
|
|
772
|
+
console.log(`${GREEN}Example added.${RESET}`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
case "drop-example":
|
|
776
|
+
case "remove-example": {
|
|
777
|
+
const machineId = rest[0] ?? "";
|
|
778
|
+
const templateId = rest[1] ?? optionalText(values.template) ?? "";
|
|
779
|
+
if (!machineId || !templateId) {
|
|
780
|
+
throw new MarketplaceError("vidfarm shop drop-example <machineId> <templateId>");
|
|
781
|
+
}
|
|
782
|
+
const result = await call(ctx, "DELETE", `${API}/shop/machines/${encodeURIComponent(machineId)}/examples/${encodeURIComponent(templateId)}`);
|
|
783
|
+
assertOk(result, "shop drop-example");
|
|
784
|
+
if (emit(ctx, result.body))
|
|
785
|
+
return;
|
|
786
|
+
console.log(`${GREEN}Example removed.${RESET}`);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
case "orders":
|
|
790
|
+
case "queue": return cmdOrders(ctx, values);
|
|
791
|
+
case "order": return cmdOrderDetail(ctx, rest[0] ?? "");
|
|
792
|
+
case "say":
|
|
793
|
+
case "comment":
|
|
794
|
+
case "reply-order": return cmdShopSay(ctx, values, rest[0] ?? "", rest.slice(1).join(" "));
|
|
795
|
+
case "deliver":
|
|
796
|
+
case "fulfil":
|
|
797
|
+
case "fulfill": return cmdDeliver(ctx, values, rest[0] ?? "");
|
|
798
|
+
case "refund": return cmdShopRefund(ctx, values, rest[0] ?? "");
|
|
799
|
+
case "reviews": return cmdShopReviews(ctx);
|
|
800
|
+
case "reply": return cmdShopReply(ctx, rest[0] ?? "", rest.slice(1).join(" "));
|
|
801
|
+
case "earnings":
|
|
802
|
+
case "analytics": return cmdEarnings(ctx, values);
|
|
803
|
+
case "withdraw": return cmdWithdraw(ctx, values);
|
|
804
|
+
case "inbox":
|
|
805
|
+
case "notifications": return cmdInbox(ctx, values);
|
|
806
|
+
default:
|
|
807
|
+
console.error(`Unknown shop subcommand: ${sub}\n`);
|
|
808
|
+
console.log(SHOP_HELP);
|
|
809
|
+
process.exitCode = 1;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
813
|
+
// vidfarm purchases — THE BUYER
|
|
814
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
815
|
+
export const PURCHASES_HELP = `vidfarm purchases — buy packs of videos on the marketplace (BUYER side)
|
|
816
|
+
|
|
817
|
+
A vendor puts PACK CARDS on the shelf. You order a pack, your USDC goes into
|
|
818
|
+
escrow, they deliver, you rule on it, and accepting is what pays them.
|
|
819
|
+
|
|
820
|
+
AN ORDER ENDS THREE WAYS, and only one is reversible:
|
|
821
|
+
accept approve and pay. FINAL, in both directions.
|
|
822
|
+
changes free. Nothing moves — the vendor re-delivers and you rule again.
|
|
823
|
+
refund the escrow comes back to you. Whoever signs it pays the gas.
|
|
824
|
+
|
|
825
|
+
THE SHELF
|
|
826
|
+
purchases shelf [--shop <slug>] What is for sale → GET ${API}/shelf
|
|
827
|
+
purchases pack <shop>/<machineId> One card, its examples and reviews
|
|
828
|
+
|
|
829
|
+
BUYING
|
|
830
|
+
purchases buy <shop>/<machineId> [--packs <n>] [--notes "<brief>"]
|
|
831
|
+
--answer <requirementId>=<url> Once per field the card demands (repeatable)
|
|
832
|
+
-y Skip the confirmation
|
|
833
|
+
→ POST ${API}/purchases
|
|
834
|
+
|
|
835
|
+
YOUR ORDERS
|
|
836
|
+
purchases Everything you bought → GET ${API}/purchases
|
|
837
|
+
purchases order <orderId> One order, with its thread and files
|
|
838
|
+
purchases say <orderId> "<message>" [--reply-to <commentId>]
|
|
839
|
+
purchases files <orderId> The delivered URLs (masters after payout)
|
|
840
|
+
|
|
841
|
+
RULING ON A PACK
|
|
842
|
+
purchases open <orderId> Tear it — a one-time event
|
|
843
|
+
purchases rate <orderId> <itemId> keep|pass [--tag <why>] [--note "…"]
|
|
844
|
+
purchases accept <orderId> Approve and pay. IRREVERSIBLE.
|
|
845
|
+
purchases changes <orderId> "<what to fix>" Free. Escrow untouched.
|
|
846
|
+
purchases refund <orderId> [--ask] [--reason "…"]
|
|
847
|
+
--ask Ask the VENDOR to sign it, so it costs you no gas
|
|
848
|
+
purchases cancel-refund <orderId> Drop a standing ask
|
|
849
|
+
purchases review <orderId> --value 5 --speed 5 --quality 5 --support 5 [--comment "…"]
|
|
850
|
+
|
|
851
|
+
purchases inbox [--unseen] Deliveries, comments, refunds
|
|
852
|
+
|
|
853
|
+
Every command takes --json. Auth: \`vidfarm login\` / VIDFARM_API_KEY / --api-key.`;
|
|
854
|
+
/** `<shopSlug>/<machineId>`, or `--shop <slug>` plus a bare machine id. */
|
|
855
|
+
export function splitPackRef(ref, values) {
|
|
856
|
+
const raw = String(ref ?? "").trim();
|
|
857
|
+
const explicitShop = optionalText(values.shop)?.trim() ?? "";
|
|
858
|
+
if (raw.includes("/")) {
|
|
859
|
+
const [shop, ...machine] = raw.split("/");
|
|
860
|
+
return { shop: shop.trim(), machine: machine.join("/").trim() };
|
|
861
|
+
}
|
|
862
|
+
if (explicitShop && raw)
|
|
863
|
+
return { shop: explicitShop, machine: raw };
|
|
864
|
+
throw new MarketplaceError("Name the pack as <shopSlug>/<machineId> — `vidfarm purchases shelf` lists both.");
|
|
865
|
+
}
|
|
866
|
+
async function cmdShelf(ctx, values) {
|
|
867
|
+
const result = await call(ctx, "GET", `${API}/shelf`, {
|
|
868
|
+
query: { shop: optionalText(values.shop), limit: optionalText(values.limit) }
|
|
869
|
+
});
|
|
870
|
+
assertOk(result, "purchases shelf");
|
|
871
|
+
if (emit(ctx, result.body))
|
|
872
|
+
return;
|
|
873
|
+
const packs = result.body.packs ?? [];
|
|
874
|
+
if (!packs.length) {
|
|
875
|
+
console.log(`${DIM}Nothing on the shelf right now.${RESET}`);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
console.log(`${BOLD}${packs.length} pack${packs.length === 1 ? "" : "s"} for sale${RESET}`);
|
|
879
|
+
for (const pack of packs) {
|
|
880
|
+
console.log(`\n ${BOLD}${pack.title}${RESET} ${DIM}by ${pack.shop.display_name}${RESET}`);
|
|
881
|
+
console.log(` ${money(pack.price_per_lot_usd)} for ${pack.lot_size} videos`
|
|
882
|
+
+ `${pack.delivery_estimate ? ` ${DIM}· ${pack.delivery_estimate}${RESET}` : ""}`);
|
|
883
|
+
if (pack.note)
|
|
884
|
+
console.log(` ${DIM}${pack.note}${RESET}`);
|
|
885
|
+
console.log(` ${DIM}buy:${RESET} ${BOLD}vidfarm purchases buy ${pack.shop_slug}/${pack.machine_id}${RESET}`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
async function cmdPack(ctx, values, ref) {
|
|
889
|
+
const { shop, machine } = splitPackRef(ref, values);
|
|
890
|
+
const result = await call(ctx, "GET", `${API}/shelf/${encodeURIComponent(shop)}/${encodeURIComponent(machine)}`);
|
|
891
|
+
assertOk(result, "purchases pack");
|
|
892
|
+
if (emit(ctx, result.body))
|
|
893
|
+
return;
|
|
894
|
+
const pack = result.body.pack;
|
|
895
|
+
console.log(`${BOLD}${pack.title}${RESET} ${DIM}by ${result.body.shop.display_name}${RESET}`);
|
|
896
|
+
console.log(` ${money(pack.price_per_lot_usd)} for ${pack.lot_size} videos`
|
|
897
|
+
+ `${pack.delivery_estimate ? ` · ${pack.delivery_estimate}` : ""}`);
|
|
898
|
+
if (pack.note)
|
|
899
|
+
console.log(` ${pack.note}`);
|
|
900
|
+
if (pack.before_you_buy_note) {
|
|
901
|
+
console.log(`\n ${YELLOW}Before you buy${RESET}\n ${pack.before_you_buy_note}`);
|
|
902
|
+
}
|
|
903
|
+
if (pack.requirements?.length) {
|
|
904
|
+
console.log(`\n ${BOLD}You must supply${RESET}`);
|
|
905
|
+
for (const row of pack.requirements) {
|
|
906
|
+
console.log(` --answer ${row.id}=<url> ${row.label}${row.hint ? ` ${DIM}(${row.hint})${RESET}` : ""}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
const summary = result.body.review_summary ?? {};
|
|
910
|
+
if (summary.count)
|
|
911
|
+
console.log(`\n ${BOLD}${summary.overall}/5${RESET} ${DIM}from ${summary.count} review(s)${RESET}`);
|
|
912
|
+
for (const example of pack.examples ?? []) {
|
|
913
|
+
console.log(` example: ${CYAN}${example.video_url ?? example.poster_url ?? example.template_id}${RESET}`);
|
|
914
|
+
}
|
|
915
|
+
console.log(`\n ${CYAN}${pack.machine_url}${RESET}`);
|
|
916
|
+
}
|
|
917
|
+
async function cmdBuy(ctx, values, ref) {
|
|
918
|
+
const { shop, machine } = splitPackRef(ref, values);
|
|
919
|
+
const packs = Math.max(1, Math.round(optionalNumber(values.packs) ?? 1));
|
|
920
|
+
// Read the card FIRST. The price comes off the machine on the server either
|
|
921
|
+
// way, so this is not about arithmetic — it is so the confirmation names the
|
|
922
|
+
// real number, and so a missing `--answer` is caught before any money moves.
|
|
923
|
+
const card = await call(ctx, "GET", `${API}/shelf/${encodeURIComponent(shop)}/${encodeURIComponent(machine)}`);
|
|
924
|
+
assertOk(card, "purchases buy");
|
|
925
|
+
const pack = card.body.pack;
|
|
926
|
+
const answers = {};
|
|
927
|
+
for (const raw of values.answer ?? []) {
|
|
928
|
+
const index = String(raw).indexOf("=");
|
|
929
|
+
if (index < 1)
|
|
930
|
+
throw new MarketplaceError(`--answer must be <requirementId>=<url>, got "${raw}".`);
|
|
931
|
+
answers[String(raw).slice(0, index).trim()] = String(raw).slice(index + 1).trim();
|
|
932
|
+
}
|
|
933
|
+
const missing = (pack.requirements ?? []).filter((row) => !answers[row.id]);
|
|
934
|
+
if (missing.length) {
|
|
935
|
+
throw new MarketplaceError([
|
|
936
|
+
`This vendor will not start without ${missing.length} more link(s):`,
|
|
937
|
+
...missing.map((row) => ` --answer ${row.id}=<url> ${row.label}`)
|
|
938
|
+
].join("\n"));
|
|
939
|
+
}
|
|
940
|
+
const total = Math.round(packs * Number(pack.price_per_lot_usd) * 100) / 100;
|
|
941
|
+
if (!values.yes && !ctx.json) {
|
|
942
|
+
console.log(`${BOLD}${packs} × ${pack.title}${RESET} from ${card.body.shop.display_name}`);
|
|
943
|
+
console.log(` ${packs * pack.lot_size} videos · ${BOLD}${money(total)}${RESET} into escrow now`);
|
|
944
|
+
if (pack.before_you_buy_note)
|
|
945
|
+
console.log(` ${YELLOW}${pack.before_you_buy_note}${RESET}`);
|
|
946
|
+
console.log(`\n ${DIM}Add -y to place it.${RESET}`);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const result = await call(ctx, "POST", `${API}/purchases`, {
|
|
950
|
+
body: {
|
|
951
|
+
shop_slug: shop,
|
|
952
|
+
machine_id: machine,
|
|
953
|
+
packs,
|
|
954
|
+
notes: optionalText(values.notes) ?? "",
|
|
955
|
+
answers,
|
|
956
|
+
// One key per attempt. A retry of THIS command reuses it, so a flaky
|
|
957
|
+
// connection cannot mint a second deposit for the same order.
|
|
958
|
+
idempotency_key: `devcli_${shop}_${machine}_${packs}_${Date.now()}`
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
// A funded-but-unrecorded failure still carries an order id, and that id is
|
|
962
|
+
// the difference between a recoverable order and a payment nobody can name.
|
|
963
|
+
if (!result.ok || result.body?.ok === false) {
|
|
964
|
+
const lines = [`purchases buy failed (${result.status}) — ${result.body?.error ?? "unknown error"}.`];
|
|
965
|
+
if (result.body?.order_id) {
|
|
966
|
+
lines.push(` The order exists: ${result.body.order_id}`);
|
|
967
|
+
lines.push(` ${result.body.funded ? "YOUR MONEY MOVED." : "No deposit was taken."}`
|
|
968
|
+
+ ` vidfarm purchases order ${result.body.order_id}`);
|
|
969
|
+
}
|
|
970
|
+
if (result.body?.retryable)
|
|
971
|
+
lines.push(" This one is safe to try again.");
|
|
972
|
+
throw new MarketplaceError(lines.join("\n"));
|
|
973
|
+
}
|
|
974
|
+
if (emit(ctx, result.body))
|
|
975
|
+
return;
|
|
976
|
+
console.log(`${GREEN}${result.body.replay ? "Already ordered." : "Ordered."}${RESET}`
|
|
977
|
+
+ ` ${BOLD}${result.body.order_id}${RESET}`);
|
|
978
|
+
console.log(` ${result.body.video_count} videos · ${money(result.body.total_usd)}`
|
|
979
|
+
+ ` · ${result.body.funded ? `${GREEN}funded${RESET}` : `${YELLOW}NOT funded — no deposit was taken${RESET}`}`);
|
|
980
|
+
console.log(` ${CYAN}${ctx.host}${result.body.order_url}${RESET}`);
|
|
981
|
+
}
|
|
982
|
+
async function cmdPurchasesList(ctx) {
|
|
983
|
+
const result = await call(ctx, "GET", `${API}/purchases`);
|
|
984
|
+
assertOk(result, "purchases");
|
|
985
|
+
if (emit(ctx, result.body))
|
|
986
|
+
return;
|
|
987
|
+
const rows = result.body.purchases ?? [];
|
|
988
|
+
if (!rows.length) {
|
|
989
|
+
console.log(`${DIM}Nothing bought yet — vidfarm purchases shelf${RESET}`);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
for (const row of rows) {
|
|
993
|
+
console.log(` ${BOLD}${row.order_id ?? row.delivery_id}${RESET} ${row.machine_title}`
|
|
994
|
+
+ ` ${DIM}from ${row.shop_name} · ${money(row.total_usd)} · ${shortDate(row.created_at)}${RESET}`
|
|
995
|
+
+ ` ${row.opened_at ? `${DIM}opened${RESET}` : `${CYAN}sealed${RESET}`} ${row.status}`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async function fetchOrder(ctx, orderId) {
|
|
999
|
+
if (!orderId)
|
|
1000
|
+
throw new MarketplaceError("Which order? Run `vidfarm purchases` to list them.");
|
|
1001
|
+
const result = await call(ctx, "GET", `${API}/purchases/${encodeURIComponent(orderId)}`);
|
|
1002
|
+
assertOk(result, "purchases order");
|
|
1003
|
+
return result.body;
|
|
1004
|
+
}
|
|
1005
|
+
async function cmdPurchaseDetail(ctx, orderId) {
|
|
1006
|
+
const body = await fetchOrder(ctx, orderId);
|
|
1007
|
+
if (emit(ctx, body))
|
|
1008
|
+
return;
|
|
1009
|
+
const { order, delivery, thread, review, can } = body;
|
|
1010
|
+
printOrderRow(order);
|
|
1011
|
+
console.log(` ${CYAN}${order.buyer_url}${RESET}`);
|
|
1012
|
+
if (delivery) {
|
|
1013
|
+
console.log(`\n${BOLD}The pack${RESET} ${DIM}${delivery.items.length} item(s)`
|
|
1014
|
+
+ `${delivery.opened_at ? "" : " · still sealed"}${RESET}`);
|
|
1015
|
+
if (!delivery.masters_released) {
|
|
1016
|
+
console.log(` ${YELLOW}Masters are locked until you accept and the payout lands.${RESET}`);
|
|
1017
|
+
}
|
|
1018
|
+
for (const item of delivery.items) {
|
|
1019
|
+
const verdict = item.verdict === "approved"
|
|
1020
|
+
? `${GREEN}kept${RESET}`
|
|
1021
|
+
: item.verdict === "rejected" ? `${RED}passed${RESET}` : `${DIM}not ruled on${RESET}`;
|
|
1022
|
+
console.log(` ${BOLD}${item.item_id}${RESET} ${verdict}${item.note ? ` ${DIM}${item.note}${RESET}` : ""}`);
|
|
1023
|
+
for (const url of item.preview_urls)
|
|
1024
|
+
console.log(` preview ${CYAN}${url}${RESET}`);
|
|
1025
|
+
for (const url of item.master_urls)
|
|
1026
|
+
console.log(` master ${CYAN}${url}${RESET}`);
|
|
1027
|
+
}
|
|
1028
|
+
if (body.zip_url)
|
|
1029
|
+
console.log(` ${DIM}all of it: ${body.zip_url}${RESET}`);
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
console.log(`\n${DIM}Nothing delivered yet.${RESET}`);
|
|
1033
|
+
}
|
|
1034
|
+
if (thread?.comments?.length) {
|
|
1035
|
+
console.log(`\n${BOLD}Thread${RESET}`);
|
|
1036
|
+
for (const comment of thread.comments) {
|
|
1037
|
+
console.log(` ${comment.mine ? BOLD : ""}${comment.author || comment.role}${RESET}`
|
|
1038
|
+
+ ` ${DIM}${shortDate(comment.created_at)} [${comment.id}]${RESET}`);
|
|
1039
|
+
console.log(` ${String(comment.body).replace(/\n/g, "\n ")}`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (review)
|
|
1043
|
+
console.log(`\n${BOLD}Your review${RESET} ${review.overall}/5`);
|
|
1044
|
+
const moves = [
|
|
1045
|
+
can?.accept ? `accept (pays ${money(order.total_usd)}, FINAL)` : null,
|
|
1046
|
+
can?.request_changes ? "changes (free)" : null,
|
|
1047
|
+
can?.refund ? "refund" : null,
|
|
1048
|
+
can?.review ? "review" : null
|
|
1049
|
+
].filter(Boolean);
|
|
1050
|
+
if (moves.length)
|
|
1051
|
+
console.log(`\n${DIM}You can: ${moves.join(" · ")}${RESET}`);
|
|
1052
|
+
}
|
|
1053
|
+
async function cmdFiles(ctx, orderId) {
|
|
1054
|
+
const body = await fetchOrder(ctx, orderId);
|
|
1055
|
+
const items = body.delivery?.items ?? [];
|
|
1056
|
+
if (emit(ctx, { masters_released: Boolean(body.delivery?.masters_released), items }))
|
|
1057
|
+
return;
|
|
1058
|
+
if (!items.length) {
|
|
1059
|
+
console.log(`${DIM}Nothing delivered yet.${RESET}`);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
for (const item of items) {
|
|
1063
|
+
for (const url of item.master_urls.length ? item.master_urls : item.preview_urls)
|
|
1064
|
+
console.log(url);
|
|
1065
|
+
}
|
|
1066
|
+
if (!body.delivery?.masters_released) {
|
|
1067
|
+
console.error(`${YELLOW}Watermarked previews only — the masters unlock when you accept.${RESET}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
async function cmdOpenPack(ctx, orderId) {
|
|
1071
|
+
const body = await fetchOrder(ctx, orderId);
|
|
1072
|
+
const deliveryId = body.delivery?.delivery_id;
|
|
1073
|
+
if (!deliveryId)
|
|
1074
|
+
throw new MarketplaceError("There is nothing delivered on that order yet.");
|
|
1075
|
+
const result = await call(ctx, "POST", `${API}/deliveries/${encodeURIComponent(deliveryId)}/open`, { body: {} });
|
|
1076
|
+
assertOk(result, "purchases open");
|
|
1077
|
+
if (emit(ctx, result.body))
|
|
1078
|
+
return;
|
|
1079
|
+
console.log(`${GREEN}Torn open.${RESET} ${body.delivery.items.length} item(s) inside.`);
|
|
1080
|
+
console.log(`${DIM}Rule on each: vidfarm purchases rate ${orderId} <itemId> keep|pass${RESET}`);
|
|
1081
|
+
}
|
|
1082
|
+
async function cmdRate(ctx, values, orderId, itemId, verdictWord) {
|
|
1083
|
+
const word = String(verdictWord ?? "").toLowerCase();
|
|
1084
|
+
const verdict = ["keep", "approve", "approved", "yes"].includes(word)
|
|
1085
|
+
? "approved"
|
|
1086
|
+
: ["pass", "reject", "rejected", "no"].includes(word) ? "rejected" : "";
|
|
1087
|
+
if (!itemId || !verdict) {
|
|
1088
|
+
throw new MarketplaceError("vidfarm purchases rate <orderId> <itemId> keep|pass");
|
|
1089
|
+
}
|
|
1090
|
+
const body = await fetchOrder(ctx, orderId);
|
|
1091
|
+
const deliveryId = body.delivery?.delivery_id;
|
|
1092
|
+
if (!deliveryId)
|
|
1093
|
+
throw new MarketplaceError("There is nothing delivered on that order yet.");
|
|
1094
|
+
const result = await call(ctx, "POST", `${API}/deliveries/${encodeURIComponent(deliveryId)}/verdict`, {
|
|
1095
|
+
body: {
|
|
1096
|
+
item_id: itemId,
|
|
1097
|
+
verdict,
|
|
1098
|
+
rejection_tag: optionalText(values.tag) ?? null,
|
|
1099
|
+
note: optionalText(values.note) ?? null
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
assertOk(result, "purchases rate");
|
|
1103
|
+
if (emit(ctx, result.body))
|
|
1104
|
+
return;
|
|
1105
|
+
console.log(`${GREEN}Recorded.${RESET} ${result.body.decided}/${result.body.total} ruled on.`);
|
|
1106
|
+
if (result.body.complete) {
|
|
1107
|
+
// Deliberately BOTH doors. A verdict is vidfarm's note of what the buyer
|
|
1108
|
+
// thought; accepting pays for the pack in full whatever those notes say.
|
|
1109
|
+
console.log(`${DIM}All of them. Now: vidfarm purchases accept ${orderId}${RESET}`);
|
|
1110
|
+
console.log(`${DIM} or: vidfarm purchases changes ${orderId} "<what to fix>" (free)${RESET}`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
async function cmdAccept(ctx, values, orderId) {
|
|
1114
|
+
const body = await fetchOrder(ctx, orderId);
|
|
1115
|
+
if (!values.yes && !ctx.json) {
|
|
1116
|
+
// The one command in this file that spends money irreversibly. It gets a
|
|
1117
|
+
// confirmation for the same reason the web card has a dialog.
|
|
1118
|
+
console.log(`${BOLD}Accepting pays ${money(body.order.total_usd)} to ${body.order.shop_name}, in full.${RESET}`);
|
|
1119
|
+
console.log(` Dollar Platoon's verdict is FINAL — there is no undo, and no partial payment.`);
|
|
1120
|
+
const rejected = (body.delivery?.items ?? []).filter((item) => item.verdict === "rejected").length;
|
|
1121
|
+
if (rejected) {
|
|
1122
|
+
console.log(` ${YELLOW}You marked ${rejected} video(s) wrong. Accepting pays for them anyway.${RESET}`);
|
|
1123
|
+
console.log(` ${YELLOW}Free alternative: vidfarm purchases changes ${orderId} "<what to fix>"${RESET}`);
|
|
1124
|
+
}
|
|
1125
|
+
console.log(`\n ${DIM}Add -y to accept.${RESET}`);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/accept`, { body: {} });
|
|
1129
|
+
assertOk(result, "purchases accept");
|
|
1130
|
+
if (emit(ctx, result.body))
|
|
1131
|
+
return;
|
|
1132
|
+
console.log(`${GREEN}${result.body.message}${RESET}`);
|
|
1133
|
+
if (!result.body.settled) {
|
|
1134
|
+
// Approved-but-unsettled is real and recoverable. Never tell this buyer to
|
|
1135
|
+
// accept again — there is nothing left to approve.
|
|
1136
|
+
console.log(`${YELLOW}The verdict landed but the payout did not. Do NOT accept again — retry the payout shortly.${RESET}`);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async function cmdChanges(ctx, orderId, note) {
|
|
1140
|
+
if (!orderId || !note)
|
|
1141
|
+
throw new MarketplaceError('vidfarm purchases changes <orderId> "<what to fix>"');
|
|
1142
|
+
const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/request-changes`, { body: { note } });
|
|
1143
|
+
assertOk(result, "purchases changes");
|
|
1144
|
+
if (emit(ctx, result.body))
|
|
1145
|
+
return;
|
|
1146
|
+
console.log(`${GREEN}${result.body.message}${RESET}`);
|
|
1147
|
+
}
|
|
1148
|
+
async function cmdBuyerRefund(ctx, values, orderId) {
|
|
1149
|
+
if (!orderId)
|
|
1150
|
+
throw new MarketplaceError("Which order? vidfarm purchases refund <orderId>");
|
|
1151
|
+
const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/refund`, {
|
|
1152
|
+
body: { mode: values.ask ? "ask" : "self", reason: optionalText(values.reason) ?? "" }
|
|
1153
|
+
});
|
|
1154
|
+
assertOk(result, "purchases refund");
|
|
1155
|
+
if (emit(ctx, result.body))
|
|
1156
|
+
return;
|
|
1157
|
+
console.log(`${GREEN}${result.body.message}${RESET}`);
|
|
1158
|
+
if (result.body.tx_hash)
|
|
1159
|
+
console.log(` ${DIM}tx ${result.body.tx_hash}${RESET}`);
|
|
1160
|
+
}
|
|
1161
|
+
async function cmdReview(ctx, values, orderId) {
|
|
1162
|
+
const axes = ["value", "speed", "quality", "support"];
|
|
1163
|
+
const scores = {};
|
|
1164
|
+
for (const axis of axes) {
|
|
1165
|
+
const score = optionalNumber(values[axis]);
|
|
1166
|
+
if (score === undefined) {
|
|
1167
|
+
throw new MarketplaceError("Score all four: --value --speed --quality --support, each 1-5."
|
|
1168
|
+
+ "\n A missing axis would be saved as a one-star you did not write.");
|
|
1169
|
+
}
|
|
1170
|
+
scores[axis] = score;
|
|
1171
|
+
}
|
|
1172
|
+
const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/review`, {
|
|
1173
|
+
body: { ...scores, comment: optionalText(values.comment) ?? "" }
|
|
1174
|
+
});
|
|
1175
|
+
assertOk(result, "purchases review");
|
|
1176
|
+
if (emit(ctx, result.body))
|
|
1177
|
+
return;
|
|
1178
|
+
console.log(result.body.duplicate
|
|
1179
|
+
? `${DIM}You already reviewed this order.${RESET}`
|
|
1180
|
+
: `${GREEN}Posted. ${result.body.overall}/5${RESET}`);
|
|
1181
|
+
}
|
|
1182
|
+
export async function runPurchasesCommand(argv) {
|
|
1183
|
+
const sub = (argv[0] ?? "").toLowerCase();
|
|
1184
|
+
if (["help", "--help", "-h"].includes(sub)) {
|
|
1185
|
+
console.log(PURCHASES_HELP);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: purchasesOptions() });
|
|
1189
|
+
const values = parsed.values;
|
|
1190
|
+
const rest = parsed.positionals.slice(1);
|
|
1191
|
+
const ctx = resolveCtx(values);
|
|
1192
|
+
switch (sub) {
|
|
1193
|
+
case "":
|
|
1194
|
+
case "list":
|
|
1195
|
+
case "orders": return cmdPurchasesList(ctx);
|
|
1196
|
+
case "shelf":
|
|
1197
|
+
case "browse":
|
|
1198
|
+
case "packs": return cmdShelf(ctx, values);
|
|
1199
|
+
case "pack":
|
|
1200
|
+
case "card": return cmdPack(ctx, values, rest[0] ?? "");
|
|
1201
|
+
case "buy":
|
|
1202
|
+
case "order-pack": return cmdBuy(ctx, values, rest[0] ?? "");
|
|
1203
|
+
case "order":
|
|
1204
|
+
case "show": return cmdPurchaseDetail(ctx, rest[0] ?? "");
|
|
1205
|
+
case "files":
|
|
1206
|
+
case "download": return cmdFiles(ctx, rest[0] ?? "");
|
|
1207
|
+
case "say":
|
|
1208
|
+
case "comment": {
|
|
1209
|
+
const orderId = rest[0] ?? "";
|
|
1210
|
+
const message = rest.slice(1).join(" ");
|
|
1211
|
+
if (!orderId || !message)
|
|
1212
|
+
throw new MarketplaceError('vidfarm purchases say <orderId> "<message>"');
|
|
1213
|
+
const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/comment`, { body: { body: message, parent_id: optionalText(values["reply-to"]) ?? "" } });
|
|
1214
|
+
assertOk(result, "purchases say");
|
|
1215
|
+
if (emit(ctx, result.body))
|
|
1216
|
+
return;
|
|
1217
|
+
console.log(`${GREEN}Posted.${RESET}`);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
case "open":
|
|
1221
|
+
case "tear": return cmdOpenPack(ctx, rest[0] ?? "");
|
|
1222
|
+
case "rate":
|
|
1223
|
+
case "verdict": return cmdRate(ctx, values, rest[0] ?? "", rest[1] ?? "", rest[2] ?? "");
|
|
1224
|
+
case "accept":
|
|
1225
|
+
case "approve": return cmdAccept(ctx, values, rest[0] ?? "");
|
|
1226
|
+
case "changes":
|
|
1227
|
+
case "request-changes":
|
|
1228
|
+
case "revise": return cmdChanges(ctx, rest[0] ?? "", rest.slice(1).join(" ") || (optionalText(values.note) ?? ""));
|
|
1229
|
+
case "refund":
|
|
1230
|
+
case "cancel": return cmdBuyerRefund(ctx, values, rest[0] ?? "");
|
|
1231
|
+
case "cancel-refund":
|
|
1232
|
+
case "drop-refund": {
|
|
1233
|
+
const orderId = rest[0] ?? "";
|
|
1234
|
+
if (!orderId)
|
|
1235
|
+
throw new MarketplaceError("Which order? vidfarm purchases cancel-refund <orderId>");
|
|
1236
|
+
const result = await call(ctx, "POST", `${API}/purchases/${encodeURIComponent(orderId)}/refund/cancel-request`, { body: {} });
|
|
1237
|
+
assertOk(result, "purchases cancel-refund");
|
|
1238
|
+
if (emit(ctx, result.body))
|
|
1239
|
+
return;
|
|
1240
|
+
console.log(`${GREEN}${result.body.message}${RESET}`);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
case "review":
|
|
1244
|
+
case "rate-vendor": return cmdReview(ctx, values, rest[0] ?? "");
|
|
1245
|
+
case "inbox":
|
|
1246
|
+
case "notifications": return cmdInbox(ctx, values);
|
|
1247
|
+
default:
|
|
1248
|
+
console.error(`Unknown purchases subcommand: ${sub}\n`);
|
|
1249
|
+
console.log(PURCHASES_HELP);
|
|
1250
|
+
process.exitCode = 1;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
//# sourceMappingURL=marketplace-console.js.map
|