@wji.ai/cli 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +889 -13
- package/package.json +10 -9
- package/skill/backreel/SKILL.md +69 -3
- package/skill/backreel/reference/jobs.md +154 -0
- package/skill/npc-character/SKILL.md +1 -1
- package/skill/short-drama/SKILL.md +9 -2
- package/skill/short-drama/examples/night-shift/project.json +5 -4
- package/skill/short-drama/reference/cinematography-low-key.md +5 -6
- package/skill/short-drama/reference/cinematography.md +1 -1
- package/skill/short-drama/reference/craft-motion.md +21 -1
- package/skill/short-drama/reference/craft-shots.md +24 -0
- package/skill/short-drama/reference/schema.md +5 -1
- package/skill/short-drama/reference/stage-characters.md +26 -16
- package/skill/short-drama/reference/stage-storyboard.md +31 -5
- package/skill/short-drama/reference/workspace-layout.md +1 -1
- package/skill/short-drama/scripts/selftest.mjs +117 -8
- package/skill/short-drama/scripts/short-drama.mjs +235 -26
- package/skill/wji/SKILL.md +5 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command27 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/app.ts
|
|
7
7
|
import { createRequire } from "module";
|
|
@@ -490,6 +490,89 @@ var ApiClient = class {
|
|
|
490
490
|
}
|
|
491
491
|
};
|
|
492
492
|
|
|
493
|
+
// ../core/src/markup.ts
|
|
494
|
+
function escapeHtml(value) {
|
|
495
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
496
|
+
}
|
|
497
|
+
function safeUrl(url, opts) {
|
|
498
|
+
const trimmed = url.trim();
|
|
499
|
+
if (/^(https?:|mailto:)/i.test(trimmed)) return trimmed;
|
|
500
|
+
if (/^\/(?!\/)/.test(trimmed) && opts.siteBase) {
|
|
501
|
+
return `${opts.siteBase.replace(/\/$/, "")}${trimmed}`;
|
|
502
|
+
}
|
|
503
|
+
return "";
|
|
504
|
+
}
|
|
505
|
+
function inline(text, opts) {
|
|
506
|
+
let html = escapeHtml(text);
|
|
507
|
+
html = html.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_, alt, url) => {
|
|
508
|
+
const src = safeUrl(url, opts);
|
|
509
|
+
return src ? `<img src="${src}" alt="${alt}">` : "";
|
|
510
|
+
});
|
|
511
|
+
html = html.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => {
|
|
512
|
+
const href = safeUrl(url, opts);
|
|
513
|
+
return href ? `<a href="${href}">${label}</a>` : label;
|
|
514
|
+
});
|
|
515
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
516
|
+
html = html.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
|
|
517
|
+
return html;
|
|
518
|
+
}
|
|
519
|
+
function markupToHtml(source, opts = {}) {
|
|
520
|
+
const inl = (text) => inline(text, opts);
|
|
521
|
+
const blocks = source.replace(/\r\n?/g, "\n").split(/\n{2,}/);
|
|
522
|
+
const out = [];
|
|
523
|
+
for (const raw of blocks) {
|
|
524
|
+
const block = raw.trim();
|
|
525
|
+
if (!block) continue;
|
|
526
|
+
const lines = block.split("\n");
|
|
527
|
+
const heading = /^(#{2,4})\s+(.*)$/.exec(block);
|
|
528
|
+
if (heading && lines.length === 1) {
|
|
529
|
+
const level = Math.min(heading[1].length, 4);
|
|
530
|
+
out.push(`<h${level}>${inl(heading[2])}</h${level}>`);
|
|
531
|
+
} else if (lines.every((l) => /^\s*[-*]\s+/.test(l))) {
|
|
532
|
+
out.push(`<ul>${lines.map((l) => `<li>${inl(l.replace(/^\s*[-*]\s+/, ""))}</li>`).join("")}</ul>`);
|
|
533
|
+
} else if (lines.every((l) => /^\s*\d+[.)]\s+/.test(l))) {
|
|
534
|
+
out.push(`<ol>${lines.map((l) => `<li>${inl(l.replace(/^\s*\d+[.)]\s+/, ""))}</li>`).join("")}</ol>`);
|
|
535
|
+
} else if (lines.every((l) => /^>\s?/.test(l))) {
|
|
536
|
+
out.push(`<blockquote><p>${lines.map((l) => inl(l.replace(/^>\s?/, ""))).join("<br>")}</p></blockquote>`);
|
|
537
|
+
} else {
|
|
538
|
+
out.push(`<p>${lines.map(inl).join("<br>")}</p>`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return out.join("");
|
|
542
|
+
}
|
|
543
|
+
function decode(value) {
|
|
544
|
+
return value.replaceAll(" ", " ").replaceAll(""", '"').replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
545
|
+
}
|
|
546
|
+
function attr(attrs, name) {
|
|
547
|
+
const m = new RegExp(`${name}\\s*=\\s*"([^"]*)"`, "i").exec(attrs);
|
|
548
|
+
return m ? decode(m[1]) : "";
|
|
549
|
+
}
|
|
550
|
+
function inlineBack(html) {
|
|
551
|
+
return decode(
|
|
552
|
+
html.replace(/<br\s*\/?>/gi, "\n").replace(/<img\b([^>]*)>/gi, (_, a) => `})`).replace(/<a\b([^>]*)>(.*?)<\/a>/gi, (_, a, label) => `[${label}](${attr(a, "href")})`).replace(/<(strong|b)>(.*?)<\/\1>/gi, "**$2**").replace(/<(em|i)>(.*?)<\/\1>/gi, "*$2*").replace(/<[^>]+>/g, "")
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
function htmlToMarkup(html) {
|
|
556
|
+
const out = [];
|
|
557
|
+
const re = /<(h[2-4]|p|ul|ol|blockquote)\b[^>]*>([\s\S]*?)<\/\1>|<img\b[^>]*>/gi;
|
|
558
|
+
for (const m of html.matchAll(re)) {
|
|
559
|
+
const [full, tag, inner = ""] = m;
|
|
560
|
+
if (!tag) {
|
|
561
|
+
out.push(inlineBack(full));
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const name = tag.toLowerCase();
|
|
565
|
+
if (name.startsWith("h")) out.push(`${"#".repeat(Number(name[1]))} ${inlineBack(inner)}`);
|
|
566
|
+
else if (name === "ul" || name === "ol") {
|
|
567
|
+
const items = [...inner.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map((li) => inlineBack(li[1]));
|
|
568
|
+
out.push(items.map((it, i) => name === "ul" ? `- ${it}` : `${i + 1}. ${it}`).join("\n"));
|
|
569
|
+
} else if (name === "blockquote") {
|
|
570
|
+
out.push(inlineBack(inner.replace(/<\/?p>/gi, "")).split("\n").map((l) => `> ${l}`).join("\n"));
|
|
571
|
+
} else out.push(inlineBack(inner));
|
|
572
|
+
}
|
|
573
|
+
return out.join("\n\n");
|
|
574
|
+
}
|
|
575
|
+
|
|
493
576
|
// ../core/src/pool.ts
|
|
494
577
|
async function mapLimit(items, limit, fn) {
|
|
495
578
|
const out = new Array(items.length);
|
|
@@ -5775,7 +5858,7 @@ function npcCommand(globals2) {
|
|
|
5775
5858
|
const result = await client.get(`${RESIDENTS}/handle-available`, { handle, excludeId });
|
|
5776
5859
|
render(
|
|
5777
5860
|
result,
|
|
5778
|
-
(r) => r.available ? success(`${r.handle} is available`) : warn(`${r.handle} is not available
|
|
5861
|
+
(r) => r.available ? success(`${r.handle} is available`) : warn(`${r.handle} is not available: ${handleReason(r.reason)}`)
|
|
5779
5862
|
);
|
|
5780
5863
|
});
|
|
5781
5864
|
const job = npc.command("job").aliases(["jobs"]).description("An NPC's work history: concerts, releases, ads\u2026");
|
|
@@ -5929,7 +6012,7 @@ function npcCommand(globals2) {
|
|
|
5929
6012
|
return npc;
|
|
5930
6013
|
}
|
|
5931
6014
|
function residentOptions(cmd) {
|
|
5932
|
-
return cmd.option("--handle <handle>", "@handle (a-z 0-9 . _ -, 2\u201332)").option("--tagline <text>", "one-line tagline").option("--bio <text>", "bio (@file / - supported)").option("--image <url|assetId>", "avatar image").option("--hero-image <url|assetId>", "hero image").option("--color <hex>", "accent color").option("--status <status>", NPC_STATUSES.join(" | ")).option("--visibility <v>", NPC_VISIBILITIES.join(" | ")).option("--social <key=url...>", "ig / yt / tw / web").option("--tag <key=value...>", "card tags: rating, type, role, gender, tier, language\u2026").option("--set <key=value...>", "any other field, e.g. liveEnabled=true");
|
|
6015
|
+
return cmd.option("--handle <handle>", "@handle (a-z 0-9 . _ -, 2\u201332)").option("--tagline <text>", "one-line tagline").option("--bio <text>", "bio (@file / - supported)").option("--image <url|assetId>", "avatar image (left empty if not given; NPC City shows initials)").option("--hero-image <url|assetId>", "hero image").option("--color <hex>", "accent color").option("--status <status>", NPC_STATUSES.join(" | ")).option("--visibility <v>", NPC_VISIBILITIES.join(" | ")).option("--social <key=url...>", "ig / yt / tw / web").option("--tag <key=value...>", "card tags: rating, type, role, gender, tier, language\u2026").option("--set <key=value...>", "any other field, e.g. liveEnabled=true");
|
|
5933
6016
|
}
|
|
5934
6017
|
function jobOptions(cmd) {
|
|
5935
6018
|
return cmd.option("--about <text>", "what happened (the job's detail line)").option("--location <text>", "where").option("--url <url>", "link");
|
|
@@ -5959,7 +6042,13 @@ async function imageUrl(client, value) {
|
|
|
5959
6042
|
}
|
|
5960
6043
|
async function assertHandle(client, handle, excludeId) {
|
|
5961
6044
|
const r = await client.get(`${RESIDENTS}/handle-available`, { handle, excludeId });
|
|
5962
|
-
if (!r.available) throw new Error(`Handle ${r.handle || handle} is not available
|
|
6045
|
+
if (!r.available) throw new Error(`Handle ${r.handle || handle} is not available: ${handleReason(r.reason)}`);
|
|
6046
|
+
}
|
|
6047
|
+
function handleReason(reason) {
|
|
6048
|
+
if (reason === "taken") return "another NPC already uses it";
|
|
6049
|
+
if (reason === "reserved") return "reserved by NPC City (system / brand names like admin, official, npccity, wji, backreel)";
|
|
6050
|
+
if (reason === "invalid") return "use 2\u201332 of a-z 0-9 . _ -";
|
|
6051
|
+
return reason || "unknown reason";
|
|
5963
6052
|
}
|
|
5964
6053
|
function oneOf(value, allowed, flag) {
|
|
5965
6054
|
if (!allowed.includes(value)) throw new Error(`${flag} must be one of ${allowed.join(", ")}`);
|
|
@@ -6015,7 +6104,7 @@ function printChanges2(changes, warnings = []) {
|
|
|
6015
6104
|
}
|
|
6016
6105
|
|
|
6017
6106
|
// src/commands/backreel.ts
|
|
6018
|
-
import { Command as
|
|
6107
|
+
import { Command as Command26 } from "commander";
|
|
6019
6108
|
|
|
6020
6109
|
// src/lib/backreel.ts
|
|
6021
6110
|
var POSTS = "/backreel/post";
|
|
@@ -6439,10 +6528,155 @@ function episodeCommand(globals2) {
|
|
|
6439
6528
|
return ep;
|
|
6440
6529
|
}
|
|
6441
6530
|
|
|
6531
|
+
// src/commands/backreel-blog.ts
|
|
6532
|
+
import { Command as Command20 } from "commander";
|
|
6533
|
+
var BLOG = "/backreel/blog";
|
|
6534
|
+
var TAG_MAX = 8;
|
|
6535
|
+
async function readBody(value, asHtml) {
|
|
6536
|
+
if (value === void 0) return void 0;
|
|
6537
|
+
const text = await readValue(value);
|
|
6538
|
+
if (asHtml) return text;
|
|
6539
|
+
if (/\]\(\/(?!\/)/.test(text)) {
|
|
6540
|
+
info(color.yellow("! Links like (/blog) are removed by Backreel \u2014 use full https:// URLs"));
|
|
6541
|
+
}
|
|
6542
|
+
return markupToHtml(text);
|
|
6543
|
+
}
|
|
6544
|
+
var parseTags = (value) => value === void 0 ? void 0 : value.split(/[,,、]/).map((it) => it.trim()).filter(Boolean).slice(0, TAG_MAX);
|
|
6545
|
+
async function resolveCover(client, value) {
|
|
6546
|
+
if (value === void 0) return void 0;
|
|
6547
|
+
if (value === "none" || value === "") return null;
|
|
6548
|
+
if (/^[a-f0-9]{24}$/i.test(value)) return value;
|
|
6549
|
+
info(color.dim(`Uploading ${value}\u2026`));
|
|
6550
|
+
const asset = await client.upload("/assets/v2/upload", value, {});
|
|
6551
|
+
return asset.id;
|
|
6552
|
+
}
|
|
6553
|
+
function resolvePost(value) {
|
|
6554
|
+
if (value === void 0) return void 0;
|
|
6555
|
+
return value === "none" || value === "" ? null : value;
|
|
6556
|
+
}
|
|
6557
|
+
function printList(rows) {
|
|
6558
|
+
table(rows, [
|
|
6559
|
+
{ header: "id", value: (b) => b.id },
|
|
6560
|
+
{ header: "title", value: (b) => truncate(b.title, 34) },
|
|
6561
|
+
{ header: "kind", value: (b) => b.kind === "official" ? color.cyan("official") : "member" },
|
|
6562
|
+
{
|
|
6563
|
+
header: "status",
|
|
6564
|
+
value: (b) => [b.status, b.pinned ? "pinned" : "", b.hidden ? color.yellow("hidden") : ""].filter(Boolean).join(" ")
|
|
6565
|
+
},
|
|
6566
|
+
{ header: "project", value: (b) => truncate(b.post?.title, 20) },
|
|
6567
|
+
{ header: "author", value: (b) => truncate(b.author?.name, 16) },
|
|
6568
|
+
{ header: "published", value: (b) => relativeTime(b.publishedAt) ?? color.dim("\u2014") }
|
|
6569
|
+
]);
|
|
6570
|
+
}
|
|
6571
|
+
function printBlog(b, opts) {
|
|
6572
|
+
detail([
|
|
6573
|
+
["id", b.id],
|
|
6574
|
+
["title", b.title],
|
|
6575
|
+
["kind", b.kind],
|
|
6576
|
+
["status", b.status],
|
|
6577
|
+
["hidden", b.hidden ? `yes \u2014 ${b.hiddenReason || "no reason given"}` : void 0],
|
|
6578
|
+
["pinned", b.pinned ? "yes" : void 0],
|
|
6579
|
+
["project", b.post ? `${b.post.title} (${b.post.id})` : "profile"],
|
|
6580
|
+
["author", b.author ? `${b.author.name} (${b.author.id})` : void 0],
|
|
6581
|
+
["tags", (b.tags || []).join(", ") || void 0],
|
|
6582
|
+
["cover", b.cover?.url],
|
|
6583
|
+
["excerpt", b.excerpt],
|
|
6584
|
+
["published", b.publishedAt],
|
|
6585
|
+
["updated", b.updatedAt]
|
|
6586
|
+
]);
|
|
6587
|
+
process.stdout.write(`
|
|
6588
|
+
${opts.html ? b.body : htmlToMarkup(b.body || "")}
|
|
6589
|
+
`);
|
|
6590
|
+
}
|
|
6591
|
+
function writeOptions(cmd) {
|
|
6592
|
+
return cmd.option("--title <text>", "title (\u2264160)").option("--body <text|@file|->", "body in Markdown (or HTML with --html); @file reads a file, - reads stdin").option("--html", "treat --body as HTML instead of Markdown").option("--excerpt <text>", "list summary (\u2264300); empty uses the start of the body").option("--tags <a,b>", `comma-separated, up to ${TAG_MAX}`).option("--cover <file|assetId|none>", "cover image: local file (uploaded), asset id, or none").option("--post <postId|none>", "post it under one of your projects; none = your profile").option("--org <orgId|none>", "write as one of your organizations; none = yourself");
|
|
6593
|
+
}
|
|
6594
|
+
function blogCommand(globals2) {
|
|
6595
|
+
const blog = new Command20("blog").aliases(["blogs", "post-updates"]).description("Blog posts on your Backreel profile or under your projects");
|
|
6596
|
+
blog.command("list", { isDefault: true }).description("Published posts; --mine lists yours including drafts and hidden ones").option("--mine", "your own posts (drafts and hidden included)").option("--official", "only Backreel press releases").option("--author <userId>", "posts by this user").option("--org <orgId>", "posts written as this organization").option("--post <postId>", "updates under this project").option("--tag <tag>", "with this tag").option("-k, --keyword <text>", "title / body").option("--page <n>", "page number", "1").option("--limit <n>", "page size", "20").action(async (opts) => {
|
|
6597
|
+
const client = backreelClient(globals2);
|
|
6598
|
+
const result = await client.get(`${BLOG}/${opts.mine ? "search/my" : "search"}`, {
|
|
6599
|
+
kind: opts.official ? "official" : void 0,
|
|
6600
|
+
author: opts.author,
|
|
6601
|
+
organization: opts.org,
|
|
6602
|
+
post: opts.post,
|
|
6603
|
+
tag: opts.tag,
|
|
6604
|
+
keyword: opts.keyword,
|
|
6605
|
+
page: Number(opts.page),
|
|
6606
|
+
limit: Number(opts.limit)
|
|
6607
|
+
});
|
|
6608
|
+
render(result, (r) => {
|
|
6609
|
+
printList(r?.data || []);
|
|
6610
|
+
const p = r?.pagination;
|
|
6611
|
+
if (p) info(color.dim(`${p.total} total \xB7 page ${p.page}${p.hasNext ? ` \xB7 more: --page ${p.page + 1}` : ""}`));
|
|
6612
|
+
});
|
|
6613
|
+
});
|
|
6614
|
+
blog.command("get <id>").alias("show").description("Show a post; the body prints as Markdown (or --html)").option("--html", "print the stored HTML body").action(async (id, opts) => {
|
|
6615
|
+
const client = backreelClient(globals2);
|
|
6616
|
+
const b = await client.getFresh(`${BLOG}/list/${id}`);
|
|
6617
|
+
render(b, () => printBlog(b, { html: !!opts.html }));
|
|
6618
|
+
});
|
|
6619
|
+
writeOptions(blog.command("create")).description("Write a post (draft unless --publish)").option("--publish", "publish now").action(async (opts) => {
|
|
6620
|
+
if (!opts.title?.trim()) throw new Error("--title is required");
|
|
6621
|
+
const client = backreelClient(globals2);
|
|
6622
|
+
if (opts.post && opts.post !== "none") await client.getFresh(`${POSTS}/list/${opts.post}`);
|
|
6623
|
+
const created = await client.post(`${BLOG}/create`, {
|
|
6624
|
+
title: opts.title,
|
|
6625
|
+
body: await readBody(opts.body, !!opts.html) ?? "",
|
|
6626
|
+
excerpt: opts.excerpt,
|
|
6627
|
+
tags: parseTags(opts.tags),
|
|
6628
|
+
cover: await resolveCover(client, opts.cover),
|
|
6629
|
+
post: resolvePost(opts.post),
|
|
6630
|
+
...opts.org !== void 0 ? { organization: opts.org === "none" ? null : opts.org } : {},
|
|
6631
|
+
status: opts.publish ? "published" : "draft"
|
|
6632
|
+
});
|
|
6633
|
+
render(created, (b) => success(`${b.status === "published" ? "Published" : "Saved draft"} "${b.title}" (${b.id})`));
|
|
6634
|
+
});
|
|
6635
|
+
writeOptions(blog.command("update <id>")).description("Change a post; only the fields you pass are changed").action(async (id, opts) => {
|
|
6636
|
+
const body = Object.fromEntries(
|
|
6637
|
+
Object.entries({
|
|
6638
|
+
title: opts.title,
|
|
6639
|
+
body: await readBody(opts.body, !!opts.html),
|
|
6640
|
+
excerpt: opts.excerpt,
|
|
6641
|
+
tags: parseTags(opts.tags),
|
|
6642
|
+
post: resolvePost(opts.post),
|
|
6643
|
+
organization: opts.org === void 0 ? void 0 : opts.org === "none" ? null : opts.org
|
|
6644
|
+
}).filter(([, v]) => v !== void 0)
|
|
6645
|
+
);
|
|
6646
|
+
const client = backreelClient(globals2);
|
|
6647
|
+
const cover = await resolveCover(client, opts.cover);
|
|
6648
|
+
if (cover !== void 0) body.cover = cover;
|
|
6649
|
+
if (!Object.keys(body).length) throw new Error("Nothing to change \u2014 pass at least one field");
|
|
6650
|
+
const updated = await client.put(`${BLOG}/list/${id}`, body);
|
|
6651
|
+
render(updated, (b) => success(`Updated "${b.title}"`));
|
|
6652
|
+
});
|
|
6653
|
+
blog.command("publish <id>").description("Publish a draft").action(async (id) => {
|
|
6654
|
+
const client = backreelClient(globals2);
|
|
6655
|
+
const updated = await client.put(`${BLOG}/list/${id}`, { status: "published" });
|
|
6656
|
+
render(updated, (b) => {
|
|
6657
|
+
success(`Published "${b.title}"`);
|
|
6658
|
+
if (b.hidden) info(color.yellow(`! An admin has hidden this post: ${b.hiddenReason || "no reason given"}`));
|
|
6659
|
+
});
|
|
6660
|
+
});
|
|
6661
|
+
blog.command("unpublish <id>").description("Move a post back to drafts").action(async (id) => {
|
|
6662
|
+
const client = backreelClient(globals2);
|
|
6663
|
+
const updated = await client.put(`${BLOG}/list/${id}`, { status: "draft" });
|
|
6664
|
+
render(updated, (b) => success(`"${b.title}" is a draft again`));
|
|
6665
|
+
});
|
|
6666
|
+
blog.command("delete <id>").alias("rm").description("Delete a post").option("-y, --yes", "skip confirmation").action(async (id, opts) => {
|
|
6667
|
+
const client = backreelClient(globals2);
|
|
6668
|
+
const b = await client.getFresh(`${BLOG}/list/${id}`);
|
|
6669
|
+
if (!await confirm(`Delete "${b.title}"?`, opts.yes)) return info("Aborted");
|
|
6670
|
+
const result = await client.delete(`${BLOG}/list/${id}`);
|
|
6671
|
+
render(result, () => success(`Deleted "${b.title}"`));
|
|
6672
|
+
});
|
|
6673
|
+
return blog;
|
|
6674
|
+
}
|
|
6675
|
+
|
|
6442
6676
|
// src/commands/backreel-enquiry.ts
|
|
6443
6677
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
6444
6678
|
import { basename as basename7 } from "path";
|
|
6445
|
-
import { Command as
|
|
6679
|
+
import { Command as Command21 } from "commander";
|
|
6446
6680
|
var ENQ = "/backreel/enquiry";
|
|
6447
6681
|
var TYPES = ["collaboration", "new_project", "investment", "licensing", "access_request", "general"];
|
|
6448
6682
|
var STATUSES = ["new", "in_discussion", "accepted", "declined", "archived", "withdrawn"];
|
|
@@ -6512,7 +6746,7 @@ function printEnquiry(e, messages) {
|
|
|
6512
6746
|
}
|
|
6513
6747
|
}
|
|
6514
6748
|
function enquiryCommand(globals2) {
|
|
6515
|
-
const enq = new
|
|
6749
|
+
const enq = new Command21("enquiry").aliases(["enquiries", "enq"]).description("Enquiries about your posts: read, reply, accept / decline, grant partner access");
|
|
6516
6750
|
enq.command("list", { isDefault: true }).description("Your inbox (received) or what you sent, newest activity first").option("--box <box>", "received | sent", "received").option("--post <postId>", "only this post").option("--status <status>", STATUSES.join(" | ")).option("--type <type>", TYPES.join(" | ")).option("--unread", "only unread").option("--archived", "show the archive instead of the inbox").option("--page <n>", "page number", "1").option("--limit <n>", "page size", "20").action(async (opts) => {
|
|
6517
6751
|
const client = backreelClient(globals2);
|
|
6518
6752
|
const result = await client.get(`${ENQ}/search`, {
|
|
@@ -6574,7 +6808,7 @@ function enquiryCommand(globals2) {
|
|
|
6574
6808
|
}
|
|
6575
6809
|
|
|
6576
6810
|
// src/commands/backreel-fund.ts
|
|
6577
|
-
import { Command as
|
|
6811
|
+
import { Command as Command22 } from "commander";
|
|
6578
6812
|
var CURRENCIES = ["hkd", "usd", "twd", "sgd", "eur", "gbp"];
|
|
6579
6813
|
var MAX_TIERS = 10;
|
|
6580
6814
|
var num = (value, flag, min = 0) => {
|
|
@@ -6662,7 +6896,7 @@ function printSupporters(post) {
|
|
|
6662
6896
|
]);
|
|
6663
6897
|
}
|
|
6664
6898
|
function fundCommand(globals2) {
|
|
6665
|
-
const fund = new
|
|
6899
|
+
const fund = new Command22("fund").aliases(["funding", "campaign"]).description("Crowdfunding campaign, donation settings and tiers, offline supporters, donation records");
|
|
6666
6900
|
fund.command("show <postId>", { isDefault: true }).description("Campaign progress, donation settings and tiers").action(async (postId) => {
|
|
6667
6901
|
const client = backreelClient(globals2);
|
|
6668
6902
|
const post = await loadPost(client, postId);
|
|
@@ -6808,6 +7042,640 @@ function fundCommand(globals2) {
|
|
|
6808
7042
|
return fund;
|
|
6809
7043
|
}
|
|
6810
7044
|
|
|
7045
|
+
// src/commands/backreel-job.ts
|
|
7046
|
+
import { Command as Command23 } from "commander";
|
|
7047
|
+
var JOBS = "/backreel/job";
|
|
7048
|
+
var CATEGORIES2 = ["short_video", "advertising", "movie", "short_drama", "music_video", "animation", "other"];
|
|
7049
|
+
var STATUSES2 = ["open", "closed", "awarded", "completed", "cancelled"];
|
|
7050
|
+
var BID_STATUSES = ["submitted", "withdrawn", "awarded", "declined"];
|
|
7051
|
+
var RESOLUTIONS = ["480p", "720p", "1080p", "2k", "4k"];
|
|
7052
|
+
var SORTS2 = ["newest", "deadline", "budget"];
|
|
7053
|
+
function parseDuration(value) {
|
|
7054
|
+
const t = value.trim().toLowerCase();
|
|
7055
|
+
if (/^\d+$/.test(t)) return Number(t);
|
|
7056
|
+
const colon = t.match(/^(\d+):(\d{1,2})$/);
|
|
7057
|
+
if (colon) return Number(colon[1]) * 60 + Number(colon[2]);
|
|
7058
|
+
const m = t.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
|
|
7059
|
+
if (!m || !t) throw new Error(`--duration "${value}" \u2014 use seconds, 3m, 1h30m or 2:30`);
|
|
7060
|
+
return Number(m[1] || 0) * 3600 + Number(m[2] || 0) * 60 + Number(m[3] || 0);
|
|
7061
|
+
}
|
|
7062
|
+
function parseDay(value, flag) {
|
|
7063
|
+
const d = /^\d{4}-\d{2}-\d{2}$/.test(value) ? /* @__PURE__ */ new Date(`${value}T23:59:00`) : new Date(value);
|
|
7064
|
+
if (Number.isNaN(d.getTime())) throw new Error(`${flag} "${value}" is not a date (YYYY-MM-DD)`);
|
|
7065
|
+
return d.toISOString();
|
|
7066
|
+
}
|
|
7067
|
+
var usd = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
7068
|
+
var fmt = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
|
|
7069
|
+
function budgetText(j) {
|
|
7070
|
+
if (j.budgetNegotiable && j.budgetMin == null && j.budgetMax == null) return "negotiable";
|
|
7071
|
+
const cur = j.currency === "usd" || !j.currency ? usd : (n) => `${String(j.currency).toUpperCase()} ${fmt(n)}`;
|
|
7072
|
+
const cash = j.budgetMin != null && j.budgetMax != null && j.budgetMin !== j.budgetMax ? `${cur(j.budgetMin)}\u2013${cur(j.budgetMax)}` : j.budgetMax != null || j.budgetMin != null ? cur(j.budgetMax ?? j.budgetMin) : "";
|
|
7073
|
+
return [cash, j.budgetCredits ? `${fmt(j.budgetCredits)} credits` : ""].filter(Boolean).join(" + ");
|
|
7074
|
+
}
|
|
7075
|
+
function jobOptions2(cmd) {
|
|
7076
|
+
return cmd.option("--category <c>", `one of ${CATEGORIES2.join(", ")}`).option("--brief <text|@file>", "what you need (max 5000)").option("--deliverables <text|@file>", "what gets delivered (max 2000)").option("--budget <min[-max]>", 'cash budget in --currency, e.g. 8000 or 5000-10000; "none" clears').option("--currency <code>", "currency of --budget (converted to USD by the server)", "USD").option("--negotiable", "budget is negotiable").option("--credits <n>", "WJI credits budget on top of cash; 0 clears").option("--duration <len>", "running time: 90, 3m, 1h30m, 2:30").option("--resolution <r>", `delivery resolution: ${RESOLUTIONS.join(", ")}`).option("--upscale", "upscaling to the resolution is acceptable").option("--no-upscale", "must be generated at the resolution").option("--model <name...>", "required WJI models (max 10)").option("--deadline <date>", "bid deadline, YYYY-MM-DD (\u2264 90 days)").option("--delivery <date>", 'delivery date, YYYY-MM-DD; "none" clears').option("--location <text>", "location, if any").option("--link <url...>", "reference links (max 5)").option("--verified-only", "only verified creators can bid").option("--open-to-all", "anyone can bid (undo --verified-only)").option("--org <orgId>", 'post as one of your organizations; "none" = personal');
|
|
7077
|
+
}
|
|
7078
|
+
async function jobBody(opts) {
|
|
7079
|
+
const body = {};
|
|
7080
|
+
if (opts.category) body.category = oneOf2(opts.category, CATEGORIES2, "--category");
|
|
7081
|
+
if (opts.brief !== void 0) body.brief = await readValue(opts.brief);
|
|
7082
|
+
if (opts.deliverables !== void 0) body.deliverables = await readValue(opts.deliverables);
|
|
7083
|
+
if (opts.budget !== void 0) {
|
|
7084
|
+
if (opts.budget === "none") Object.assign(body, { budgetMin: null, budgetMax: null });
|
|
7085
|
+
else {
|
|
7086
|
+
const [lo, hi] = String(opts.budget).split("-").map((v) => Number(v.replace(/[, ]/g, "")));
|
|
7087
|
+
if (!(lo >= 0) || hi !== void 0 && !(hi >= lo)) throw new Error("--budget must be like 8000 or 5000-10000");
|
|
7088
|
+
Object.assign(body, { budgetMin: lo, budgetMax: hi ?? lo });
|
|
7089
|
+
}
|
|
7090
|
+
body.currency = String(opts.currency || "USD").toUpperCase();
|
|
7091
|
+
}
|
|
7092
|
+
if (opts.negotiable) body.budgetNegotiable = true;
|
|
7093
|
+
if (opts.credits !== void 0) body.budgetCredits = Number(opts.credits) > 0 ? Math.round(Number(opts.credits)) : null;
|
|
7094
|
+
if (opts.duration !== void 0) body.durationSeconds = parseDuration(opts.duration);
|
|
7095
|
+
if (opts.resolution) body.resolution = oneOf2(opts.resolution, RESOLUTIONS, "--resolution");
|
|
7096
|
+
if (opts.upscale !== void 0 && process.argv.some((a) => a === "--upscale" || a === "--no-upscale")) {
|
|
7097
|
+
body.allowUpscale = !!opts.upscale;
|
|
7098
|
+
}
|
|
7099
|
+
if (opts.model) body.requiredModels = opts.model.slice(0, 10);
|
|
7100
|
+
if (opts.deadline) body.bidDeadline = parseDay(opts.deadline, "--deadline");
|
|
7101
|
+
if (opts.delivery !== void 0) body.deliveryDate = opts.delivery === "none" ? null : parseDay(opts.delivery, "--delivery");
|
|
7102
|
+
if (opts.location !== void 0) body.location = opts.location;
|
|
7103
|
+
if (opts.link) body.referenceLinks = opts.link.slice(0, 5);
|
|
7104
|
+
if (opts.verifiedOnly) body.verifiedOnly = true;
|
|
7105
|
+
if (opts.openToAll) body.verifiedOnly = false;
|
|
7106
|
+
if (opts.org !== void 0) body.organization = opts.org === "none" ? null : opts.org;
|
|
7107
|
+
return body;
|
|
7108
|
+
}
|
|
7109
|
+
function printJob(j) {
|
|
7110
|
+
const req = [
|
|
7111
|
+
j.durationSeconds ? `${Math.floor(j.durationSeconds / 60)}m${j.durationSeconds % 60 ? `${j.durationSeconds % 60}s` : ""}` : "",
|
|
7112
|
+
j.resolution ? `${j.resolution}${j.allowUpscale ? " (upscale ok)" : j.allowUpscale === false ? " (no upscale)" : ""}` : "",
|
|
7113
|
+
j.requiredModels?.length ? j.requiredModels.join(", ") : ""
|
|
7114
|
+
].filter(Boolean);
|
|
7115
|
+
detail([
|
|
7116
|
+
["id", j.id],
|
|
7117
|
+
["title", j.title],
|
|
7118
|
+
["category", j.category],
|
|
7119
|
+
["status", [j.status, j.hidden ? color.yellow("hidden") : "", j.verifiedOnly ? "verified only" : ""].filter(Boolean).join(" \xB7 ")],
|
|
7120
|
+
["poster", j.organization?.name ? `${j.organization.name} (org)` : j.poster?.name],
|
|
7121
|
+
["budget", budgetText(j)],
|
|
7122
|
+
["requirements", req.join(" \xB7 ") || color.dim("\u2014")],
|
|
7123
|
+
["bid deadline", j.bidDeadline ? `${new Date(j.bidDeadline).toLocaleString()} (${relativeTime(j.bidDeadline)})` : ""],
|
|
7124
|
+
["delivery", j.deliveryDate ? new Date(j.deliveryDate).toLocaleDateString() : ""],
|
|
7125
|
+
["location", j.location],
|
|
7126
|
+
["bids", j.bidCount],
|
|
7127
|
+
["my bids", (j.myBids || []).map((b) => `${b.id} \xB7 ${b.status}${b.organization ? ` \xB7 as ${b.organization.name}` : ""}`).join("\n") || void 0],
|
|
7128
|
+
["contract", j.contractId],
|
|
7129
|
+
["brief", j.brief],
|
|
7130
|
+
["deliverables", j.deliverables],
|
|
7131
|
+
["links", (j.referenceLinks || []).join("\n")]
|
|
7132
|
+
]);
|
|
7133
|
+
}
|
|
7134
|
+
function printJobs2(rows) {
|
|
7135
|
+
table(rows, [
|
|
7136
|
+
{ header: "id", value: (j) => j.id },
|
|
7137
|
+
{ header: "title", value: (j) => truncate(j.title, 30) },
|
|
7138
|
+
{ header: "category", value: (j) => j.category },
|
|
7139
|
+
{ header: "status", value: (j) => j.hidden ? `${j.status} ${color.yellow("hidden")}` : j.status },
|
|
7140
|
+
{ header: "budget", value: (j) => truncate(budgetText(j), 28) },
|
|
7141
|
+
{ header: "bids", value: (j) => j.bidCount ?? "" },
|
|
7142
|
+
{ header: "deadline", value: (j) => relativeTime(j.bidDeadline) }
|
|
7143
|
+
]);
|
|
7144
|
+
}
|
|
7145
|
+
function printBids(rows) {
|
|
7146
|
+
table(rows, [
|
|
7147
|
+
{ header: "bid", value: (b) => b.id },
|
|
7148
|
+
{ header: "job", value: (b) => truncate(b.job?.title, 24) },
|
|
7149
|
+
{ header: "bidder", value: (b) => truncate(b.organization?.name || b.bidder?.name, 18) },
|
|
7150
|
+
{ header: "status", value: (b) => b.status },
|
|
7151
|
+
{ header: "price", value: (b) => b.price == null ? "" : b.currency === "usd" || !b.currency ? usd(b.price) : `${b.currency.toUpperCase()} ${fmt(b.price)}` },
|
|
7152
|
+
{ header: "credits", value: (b) => fmt(b.creditsPrice) },
|
|
7153
|
+
{ header: "days", value: (b) => b.timelineDays ?? "" },
|
|
7154
|
+
{ header: "pitch", value: (b) => truncate(b.pitch, 30) }
|
|
7155
|
+
]);
|
|
7156
|
+
}
|
|
7157
|
+
var pageNote = (r) => {
|
|
7158
|
+
const p = r?.pagination;
|
|
7159
|
+
if (p) info(color.dim(`${p.total} total \xB7 page ${p.page}${p.hasNext ? ` \xB7 more: --page ${p.page + 1}` : ""}`));
|
|
7160
|
+
};
|
|
7161
|
+
function bidOptions(cmd) {
|
|
7162
|
+
return cmd.option("--price <n>", "cash quote in --currency").option("--currency <code>", "currency of --price", "USD").option("--credits <n>", "credits quote").option("--days <n>", "days to deliver").option("--pitch <text|@file>", "why you (max 2000)").option("--work <postId...>", "up to 3 of your Backreel works to show").option("--org <orgId>", 'bid as one of your organizations; "none" = personal');
|
|
7163
|
+
}
|
|
7164
|
+
async function bidBody(opts) {
|
|
7165
|
+
const body = {};
|
|
7166
|
+
if (opts.price !== void 0) {
|
|
7167
|
+
body.price = opts.price === "none" ? null : Number(opts.price);
|
|
7168
|
+
body.currency = String(opts.currency || "USD").toUpperCase();
|
|
7169
|
+
}
|
|
7170
|
+
if (opts.credits !== void 0) body.creditsPrice = Number(opts.credits) > 0 ? Math.round(Number(opts.credits)) : null;
|
|
7171
|
+
if (opts.days !== void 0) body.timelineDays = Number(opts.days);
|
|
7172
|
+
if (opts.pitch !== void 0) body.pitch = await readValue(opts.pitch);
|
|
7173
|
+
if (opts.work) body.works = opts.work.slice(0, 3);
|
|
7174
|
+
if (opts.org !== void 0) body.organization = opts.org === "none" ? null : opts.org;
|
|
7175
|
+
return body;
|
|
7176
|
+
}
|
|
7177
|
+
function jobCommand(globals2) {
|
|
7178
|
+
const job = new Command23("job").aliases(["jobs", "plaza"]).description("Job Plaza: post jobs, place sealed bids, award a bid");
|
|
7179
|
+
job.command("list", { isDefault: true }).description("Open jobs on the plaza; --mine for jobs you posted").option("--mine", "jobs you (or your org) posted, any status").option("--status <s>", `with --mine: ${STATUSES2.join(", ")}`).option("--all", "include closed / finished jobs (plaza)").option("--category <c>", `one of ${CATEGORIES2.join(", ")}`).option("--sort <s>", `${SORTS2.join(", ")}`).option("--verified-only", "only jobs limited to verified creators").option("--org <orgId>", "jobs posted by this organization").option("-k, --keyword <text>", "search text").option("--page <n>", "page", "1").option("--limit <n>", "page size", "20").action(async (opts) => {
|
|
7180
|
+
const client = backreelClient(globals2);
|
|
7181
|
+
const query = { page: Number(opts.page), limit: Number(opts.limit), organization: opts.org };
|
|
7182
|
+
if (opts.mine) {
|
|
7183
|
+
if (opts.status) query.status = oneOf2(opts.status, STATUSES2, "--status");
|
|
7184
|
+
} else {
|
|
7185
|
+
Object.assign(query, {
|
|
7186
|
+
state: opts.all ? "all" : "open",
|
|
7187
|
+
category: opts.category ? oneOf2(opts.category, CATEGORIES2, "--category") : void 0,
|
|
7188
|
+
sort: opts.sort ? oneOf2(opts.sort, SORTS2, "--sort") : void 0,
|
|
7189
|
+
verifiedOnly: opts.verifiedOnly ? true : void 0,
|
|
7190
|
+
keyword: opts.keyword
|
|
7191
|
+
});
|
|
7192
|
+
}
|
|
7193
|
+
const result = await client.getFresh(`${JOBS}/${opts.mine ? "search/my" : "search"}`, query);
|
|
7194
|
+
render(result, (r) => {
|
|
7195
|
+
printJobs2(r?.data || []);
|
|
7196
|
+
pageNote(r);
|
|
7197
|
+
});
|
|
7198
|
+
});
|
|
7199
|
+
job.command("get <jobId>").description("Show a job (and your bid on it, if any)").action(async (id) => {
|
|
7200
|
+
const client = backreelClient(globals2);
|
|
7201
|
+
const j = await client.getFresh(`${JOBS}/list/${id}`);
|
|
7202
|
+
render(j, () => printJob(j));
|
|
7203
|
+
});
|
|
7204
|
+
job.command("quota").description("How many bids you have left this month").action(async () => {
|
|
7205
|
+
const client = backreelClient(globals2);
|
|
7206
|
+
const q = await client.getFresh(`${JOBS}/quota`);
|
|
7207
|
+
render(q, () => {
|
|
7208
|
+
info(color.dim(`Month ${q.month}`));
|
|
7209
|
+
table(q.identities || [], [
|
|
7210
|
+
{ header: "bid as", value: (it) => it.type === "org" ? `${it.name} (org ${it.id})` : "you" },
|
|
7211
|
+
{ header: "used", value: (it) => `${it.used} / ${it.limit == null || it.limit === Infinity ? "\u221E" : it.limit}` },
|
|
7212
|
+
{ header: "verified", value: (it) => it.verified ? "yes" : color.dim("no") }
|
|
7213
|
+
]);
|
|
7214
|
+
});
|
|
7215
|
+
});
|
|
7216
|
+
jobOptions2(job.command("create <title>").description("Post a job (goes live immediately)")).action(
|
|
7217
|
+
async (title, opts) => {
|
|
7218
|
+
const client = backreelClient(globals2);
|
|
7219
|
+
const body = { ...await jobBody(opts), title };
|
|
7220
|
+
for (const f of ["category", "brief", "deliverables", "bidDeadline"]) {
|
|
7221
|
+
if (body[f] === void 0) throw new Error(`--${f === "bidDeadline" ? "deadline" : f} is required`);
|
|
7222
|
+
}
|
|
7223
|
+
const created = await client.post(`${JOBS}/create`, body);
|
|
7224
|
+
render(created, (j) => success(`Posted "${j.title}" \u2014 ${j.id}`));
|
|
7225
|
+
}
|
|
7226
|
+
);
|
|
7227
|
+
jobOptions2(job.command("set <jobId>").description("Edit your job; only the flags you pass change").option("--title <text>", "title")).action(
|
|
7228
|
+
async (id, opts) => {
|
|
7229
|
+
const client = backreelClient(globals2);
|
|
7230
|
+
const body = await jobBody(opts);
|
|
7231
|
+
if (opts.title) body.title = opts.title;
|
|
7232
|
+
if (!Object.keys(body).length) throw new Error("Nothing to change \u2014 pass at least one flag");
|
|
7233
|
+
const updated = await client.put(`${JOBS}/list/${id}`, body);
|
|
7234
|
+
render(updated, (j) => success(`Updated "${j.title}": ${Object.keys(body).join(", ")}`));
|
|
7235
|
+
}
|
|
7236
|
+
);
|
|
7237
|
+
const transitions = {
|
|
7238
|
+
close: "Stop taking bids early",
|
|
7239
|
+
reopen: "Take bids again (before the deadline)",
|
|
7240
|
+
cancel: "Cancel the job",
|
|
7241
|
+
complete: "Mark the job completed (jobs with a contract complete through the contract)"
|
|
7242
|
+
};
|
|
7243
|
+
for (const [action, description] of Object.entries(transitions)) {
|
|
7244
|
+
job.command(`${action} <jobId>`).description(description).option("-y, --yes", "skip confirmation").action(async (id, opts) => {
|
|
7245
|
+
const client = backreelClient(globals2);
|
|
7246
|
+
if (action === "cancel" && !await confirm(`Cancel job ${id}? Bidders are notified.`, opts.yes)) return info("Aborted");
|
|
7247
|
+
const j = await client.post(`${JOBS}/list/${id}/${action}`);
|
|
7248
|
+
render(j, () => success(`"${j.title}" is now ${j.status}`));
|
|
7249
|
+
});
|
|
7250
|
+
}
|
|
7251
|
+
job.command("bids [jobId]").description("Bids on your job (poster only \u2014 bids are sealed); without a job id, your own bids").option("--status <s>", `your bids: ${BID_STATUSES.join(", ")}`).option("--page <n>", "page", "1").option("--limit <n>", "page size", "20").action(async (id, opts) => {
|
|
7252
|
+
const client = backreelClient(globals2);
|
|
7253
|
+
if (id) {
|
|
7254
|
+
const rows = await client.getFresh(`${JOBS}/list/${id}/bids`);
|
|
7255
|
+
return render(rows, (r) => printBids(r || []));
|
|
7256
|
+
}
|
|
7257
|
+
const result = await client.getFresh(`${JOBS}/bids/my`, {
|
|
7258
|
+
page: Number(opts.page),
|
|
7259
|
+
limit: Number(opts.limit),
|
|
7260
|
+
status: opts.status ? oneOf2(opts.status, BID_STATUSES, "--status") : void 0
|
|
7261
|
+
});
|
|
7262
|
+
render(result, (r) => {
|
|
7263
|
+
printBids(r?.data || []);
|
|
7264
|
+
pageNote(r);
|
|
7265
|
+
});
|
|
7266
|
+
});
|
|
7267
|
+
bidOptions(job.command("bid <jobId>").description("Place a sealed bid (uses one of your monthly bids)")).action(
|
|
7268
|
+
async (id, opts) => {
|
|
7269
|
+
const client = backreelClient(globals2);
|
|
7270
|
+
const body = await bidBody(opts);
|
|
7271
|
+
if (body.timelineDays === void 0) throw new Error("--days is required");
|
|
7272
|
+
if (!body.pitch) throw new Error("--pitch is required");
|
|
7273
|
+
const j = await client.post(`${JOBS}/list/${id}/bids`, body);
|
|
7274
|
+
render(j, (x) => {
|
|
7275
|
+
const mine = (x.myBids || []).filter((b) => b.status === "submitted");
|
|
7276
|
+
success(`Bid placed on "${x.title}"${mine.length ? ` \u2014 bid ${mine[mine.length - 1].id}` : ""}`);
|
|
7277
|
+
});
|
|
7278
|
+
}
|
|
7279
|
+
);
|
|
7280
|
+
bidOptions(job.command("bid-set <bidId>").description("Change your bid while bidding is open")).action(
|
|
7281
|
+
async (bidId, opts) => {
|
|
7282
|
+
const client = backreelClient(globals2);
|
|
7283
|
+
const body = await bidBody(opts);
|
|
7284
|
+
if (!Object.keys(body).length) throw new Error("Nothing to change \u2014 pass at least one flag");
|
|
7285
|
+
const j = await client.put(`${JOBS}/bid/${bidId}`, body);
|
|
7286
|
+
render(j, () => success(`Updated bid ${bidId}: ${Object.keys(body).join(", ")}`));
|
|
7287
|
+
}
|
|
7288
|
+
);
|
|
7289
|
+
job.command("withdraw <bidId>").description("Withdraw your bid").action(async (bidId) => {
|
|
7290
|
+
const client = backreelClient(globals2);
|
|
7291
|
+
const j = await client.post(`${JOBS}/bid/${bidId}/withdraw`);
|
|
7292
|
+
render(j, () => success(`Withdrew bid ${bidId}`));
|
|
7293
|
+
});
|
|
7294
|
+
job.command("award <bidId>").description("Pick the winning bid: other bids are declined and a contract is drafted").option("-y, --yes", "skip confirmation").action(async (bidId, opts) => {
|
|
7295
|
+
const client = backreelClient(globals2);
|
|
7296
|
+
if (!await confirm(`Award bid ${bidId}? All other bids are declined.`, opts.yes)) return info("Aborted");
|
|
7297
|
+
const r = await client.post(`${JOBS}/bid/${bidId}/award`);
|
|
7298
|
+
render(
|
|
7299
|
+
r,
|
|
7300
|
+
(x) => success(`Awarded.${x?.contractId ? ` Contract ${x.contractId} \u2014 next: wji br contract plan ${x.contractId}` : ""}`)
|
|
7301
|
+
);
|
|
7302
|
+
});
|
|
7303
|
+
return job;
|
|
7304
|
+
}
|
|
7305
|
+
|
|
7306
|
+
// src/commands/backreel-contract.ts
|
|
7307
|
+
import { Command as Command24 } from "commander";
|
|
7308
|
+
var CONTRACTS = "/backreel/contract";
|
|
7309
|
+
var RELEASES = ["upfront", "milestones"];
|
|
7310
|
+
var CR_REASONS = ["scope", "revision", "other"];
|
|
7311
|
+
var KIND_NAMES = {
|
|
7312
|
+
preprod: "Pre-production",
|
|
7313
|
+
lookdev: "Look development",
|
|
7314
|
+
rough: "Rough cut",
|
|
7315
|
+
final: "Final delivery",
|
|
7316
|
+
concept: "Concept",
|
|
7317
|
+
draft: "Draft"
|
|
7318
|
+
};
|
|
7319
|
+
var usd2 = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
7320
|
+
var fmt2 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
|
|
7321
|
+
var msName = (m, i) => m.title || KIND_NAMES[m.kind] || `Payment ${i + 1}`;
|
|
7322
|
+
async function loadContract(client, ref) {
|
|
7323
|
+
try {
|
|
7324
|
+
return await client.getFresh(`${CONTRACTS}/${ref}`);
|
|
7325
|
+
} catch (err) {
|
|
7326
|
+
if (!/404|not found|不存在/i.test(String(err?.message))) throw err;
|
|
7327
|
+
return client.getFresh(`${CONTRACTS}/job/${ref}`);
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
function milestoneKey(c, n) {
|
|
7331
|
+
const i = Number(n);
|
|
7332
|
+
if (!Number.isInteger(i) || i < 1 || i > c.milestones.length) {
|
|
7333
|
+
throw new Error(`Payment ${n} does not exist \u2014 this contract has ${c.milestones.length}`);
|
|
7334
|
+
}
|
|
7335
|
+
return c.milestones[i - 1].key;
|
|
7336
|
+
}
|
|
7337
|
+
function nextStep(c) {
|
|
7338
|
+
const side = c.mySide;
|
|
7339
|
+
if (c.status === "planning") {
|
|
7340
|
+
if (!c.proposedBy) return `Draft the payment plan: wji br contract plan ${c.id} --payment "\u2026" --payment "\u2026"`;
|
|
7341
|
+
return c.proposedBy === side ? "Waiting for the other side to accept or edit the plan" : `Review the plan: wji br contract accept ${c.id}`;
|
|
7342
|
+
}
|
|
7343
|
+
if (c.status === "funding") return side === "poster" ? `Add the credits: wji br contract fund ${c.id}` : "Waiting for the poster to add the credits";
|
|
7344
|
+
if (c.status === "disputed") return `In dispute \u2014 an admin decides; add your side: wji br contract statement ${c.id} --text "\u2026"`;
|
|
7345
|
+
if (c.status === "in_production") {
|
|
7346
|
+
const i = c.milestones.findIndex((m2) => m2.status !== "approved" && m2.status !== "pending");
|
|
7347
|
+
if (i < 0) return "";
|
|
7348
|
+
const m = c.milestones[i];
|
|
7349
|
+
if (m.status === "submitted") return side === "poster" ? `Review payment ${i + 1}: approve or revise` : `Waiting for review of payment ${i + 1}`;
|
|
7350
|
+
return side === "creator" ? `Work on payment ${i + 1}, then: wji br contract submit ${c.id} ${i + 1}` : `Creator is working on payment ${i + 1}`;
|
|
7351
|
+
}
|
|
7352
|
+
return "";
|
|
7353
|
+
}
|
|
7354
|
+
function printContract(c) {
|
|
7355
|
+
const e = c.escrow;
|
|
7356
|
+
detail([
|
|
7357
|
+
["id", c.id],
|
|
7358
|
+
["job", `${c.job?.title} (${c.job?.id})`],
|
|
7359
|
+
["status", c.status],
|
|
7360
|
+
["you are", c.mySide],
|
|
7361
|
+
["poster", c.posterOrg?.name ? `${c.posterOrg.name} (org)` : c.poster?.name],
|
|
7362
|
+
["creator", c.creatorOrg?.name ? `${c.creatorOrg.name} (org)` : c.creator?.name],
|
|
7363
|
+
["total", `${usd2(c.totalUsd)} = cash ${usd2(c.cashUsd)} + ${fmt2(c.credits)} credits (${Math.round((c.creditsShare ?? 0) * 100)}% credits, min ${Math.round((c.minCreditsShare ?? 0.25) * 100)}%)`],
|
|
7364
|
+
["credits release", c.creditsRelease === "milestones" ? "with each payment" : "all at once"],
|
|
7365
|
+
["plan", c.status === "planning" ? `v${c.planVersion}${c.proposedBy ? ` proposed by ${c.proposedBy}` : ""}` : void 0],
|
|
7366
|
+
[
|
|
7367
|
+
"project credits",
|
|
7368
|
+
e ? e.settledAt ? `settled: ${e.settledTo === "split" ? `${fmt2(e.settledCreator)} to creator, ${fmt2(e.settledPoster)} to poster` : `${fmt2(e.settledAmount)} to ${e.settledTo}`}${e.topupReturned ? `, ${fmt2(e.topupReturned)} top-up returned` : ""}` : `${fmt2(Math.max(0, e.left))} left of ${fmt2(e.budget ?? e.funded)} (used ${fmt2(e.spent)}${e.toppedUp ? `, creator topped up ${fmt2(e.toppedUp)}` : ""}; overdraft ${fmt2(e.overdraft)})` : void 0
|
|
7369
|
+
],
|
|
7370
|
+
["workspace", e?.workspaceId],
|
|
7371
|
+
["cancel request", c.cancelRequest ? `${c.cancelRequest.side}${c.cancelRequest.reason ? `: ${c.cancelRequest.reason}` : ""}` : void 0],
|
|
7372
|
+
["creator silent since", c.creatorSilentSince ? `${new Date(c.creatorSilentSince).toLocaleDateString()} \u2014 you can cancel alone` : void 0],
|
|
7373
|
+
["next", nextStep(c) || void 0]
|
|
7374
|
+
]);
|
|
7375
|
+
info("");
|
|
7376
|
+
table(c.milestones || [], [
|
|
7377
|
+
{ header: "#", value: (_m) => String(c.milestones.indexOf(_m) + 1) },
|
|
7378
|
+
{ header: "payment", value: (m) => truncate(msName(m, c.milestones.indexOf(m)), 22) },
|
|
7379
|
+
{ header: "due", value: (m) => new Date(m.dueDate).toLocaleDateString() },
|
|
7380
|
+
{ header: "cash", value: (m) => usd2(m.cashUsd) },
|
|
7381
|
+
...c.creditsRelease === "milestones" ? [{ header: "credits", value: (m) => fmt2(m.credits) }] : [],
|
|
7382
|
+
{ header: "status", value: (m) => m.status },
|
|
7383
|
+
{ header: "revisions", value: (m) => m.revisionsUsed ? `${m.revisionsUsed}/${c.revisionAllowance}` : "" },
|
|
7384
|
+
{
|
|
7385
|
+
header: "cash paid",
|
|
7386
|
+
value: (m) => m.cashReceivedAt ? "received" : m.cashPaidAt ? "paid" : m.status === "approved" && m.cashUsd > 0 ? color.yellow("due") : ""
|
|
7387
|
+
}
|
|
7388
|
+
]);
|
|
7389
|
+
const pending = (c.changeRequests || []).find((r) => r.status === "pending");
|
|
7390
|
+
if (pending) {
|
|
7391
|
+
info("");
|
|
7392
|
+
info(`${color.yellow("Change request waiting")} ${pending.id}: +${fmt2(pending.credits)} credits${pending.cashUsd ? ` + ${usd2(pending.cashUsd)}` : ""} (${pending.reason}) \u2014 ${truncate(pending.note, 60)}`);
|
|
7393
|
+
}
|
|
7394
|
+
const d = c.dispute;
|
|
7395
|
+
if (d && d.status !== "withdrawn") {
|
|
7396
|
+
info("");
|
|
7397
|
+
info(`${d.status === "open" ? color.yellow("Dispute open") : "Dispute decided"} \u2014 raised by ${d.side}: ${truncate(d.reason, 80)}`);
|
|
7398
|
+
for (const st of d.statements || []) info(color.dim(` ${st.side} \xB7 ${relativeTime(st.at)}: ${truncate(st.text, 80)}`));
|
|
7399
|
+
if (d.resolution) info(` Decision: ${d.resolution.outcome}${d.resolution.outcome === "cancel" ? ` (${fmt2(d.resolution.creatorCredits)} credits to creator)` : ""} \u2014 ${truncate(d.resolution.note, 60)}`);
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
function parsePayment(spec, perMilestone) {
|
|
7403
|
+
const [title, due, cash, credits, ...rest] = spec.split("|").map((s) => s.trim());
|
|
7404
|
+
if (!due || cash === void 0) throw new Error(`--payment "${spec}" must be "Title|YYYY-MM-DD|cash[|credits[|deliverable]]"`);
|
|
7405
|
+
const n = Number(cash.replace(/[, ]/g, ""));
|
|
7406
|
+
if (!(n >= 0)) throw new Error(`--payment "${spec}": cash must be a number`);
|
|
7407
|
+
return {
|
|
7408
|
+
title,
|
|
7409
|
+
dueDate: parseDay(due, "--payment"),
|
|
7410
|
+
cash: n,
|
|
7411
|
+
deliverable: rest.join("|"),
|
|
7412
|
+
...perMilestone ? { credits: Math.round(Number(credits) || 0) } : {}
|
|
7413
|
+
};
|
|
7414
|
+
}
|
|
7415
|
+
function contractCommand(globals2) {
|
|
7416
|
+
const ct = new Command24("contract").aliases(["contracts", "deal"]).description("Job contracts: payment plan, project credits, milestones, disputes");
|
|
7417
|
+
const run = async (ref, path, body, done) => {
|
|
7418
|
+
const client = backreelClient(globals2);
|
|
7419
|
+
const c = await loadContract(client, ref);
|
|
7420
|
+
const updated = await client.post(`${CONTRACTS}/${c.id}${path}`, body);
|
|
7421
|
+
render(updated, (u) => {
|
|
7422
|
+
if (done) success(done(u));
|
|
7423
|
+
const step = nextStep(u);
|
|
7424
|
+
if (step) info(color.dim(`Next: ${step}`));
|
|
7425
|
+
});
|
|
7426
|
+
return updated;
|
|
7427
|
+
};
|
|
7428
|
+
ct.command("list", { isDefault: true }).description("Contracts you are part of (personal and your orgs)").option("--done", "finished ones (completed / cancelled)").option("--status <s>", "planning, funding, in_production, disputed, completed, cancelled").option("--page <n>", "page", "1").action(async (opts) => {
|
|
7429
|
+
const client = backreelClient(globals2);
|
|
7430
|
+
const result = await client.getFresh(`${CONTRACTS}/my`, {
|
|
7431
|
+
page: Number(opts.page),
|
|
7432
|
+
status: opts.status || (opts.done ? "done" : "active")
|
|
7433
|
+
});
|
|
7434
|
+
render(result, (r) => {
|
|
7435
|
+
table(r?.data || [], [
|
|
7436
|
+
{ header: "id", value: (c) => c.id },
|
|
7437
|
+
{ header: "job", value: (c) => truncate(c.job?.title, 28) },
|
|
7438
|
+
{ header: "you", value: (c) => c.mySide },
|
|
7439
|
+
{ header: "status", value: (c) => c.status },
|
|
7440
|
+
{ header: "total", value: (c) => usd2(c.totalUsd) },
|
|
7441
|
+
{ header: "progress", value: (c) => `${c.approvedCount}/${c.milestoneCount}` },
|
|
7442
|
+
{ header: "updated", value: (c) => relativeTime(c.updatedAt) }
|
|
7443
|
+
]);
|
|
7444
|
+
const p = r?.pagination;
|
|
7445
|
+
if (p) info(color.dim(`${p.total} total \xB7 page ${p.page}${p.hasNext ? ` \xB7 more: --page ${p.page + 1}` : ""}`));
|
|
7446
|
+
});
|
|
7447
|
+
});
|
|
7448
|
+
ct.command("get <contract>").description("Show a contract (contract id or job id): totals, payments, project credits, what to do next").option("--events", "also print the activity log").action(async (ref, opts) => {
|
|
7449
|
+
const client = backreelClient(globals2);
|
|
7450
|
+
const c = await loadContract(client, ref);
|
|
7451
|
+
render(c, () => {
|
|
7452
|
+
printContract(c);
|
|
7453
|
+
if (opts.events) {
|
|
7454
|
+
info("");
|
|
7455
|
+
for (const e of c.events || []) info(color.dim(`${new Date(e.at).toLocaleString()} \xB7 ${e.side || "system"} \xB7 ${e.type}`));
|
|
7456
|
+
}
|
|
7457
|
+
});
|
|
7458
|
+
});
|
|
7459
|
+
ct.command("plan <contract>").description("Propose or edit the payment plan (2\u20133 payments). Sending counts as your acceptance").option("--payment <spec...>", '"Title|YYYY-MM-DD|cash[|credits[|deliverable]]" per payment (replaces all)').option("--currency <code>", "currency of the cash amounts", "USD").option("--credits <n>", "credits for the whole project (all-at-once release)").option("--release <mode>", "upfront (all at once, default) or milestones (with each payment)").action(async (ref, opts) => {
|
|
7460
|
+
const client = backreelClient(globals2);
|
|
7461
|
+
const c = await loadContract(client, ref);
|
|
7462
|
+
const release = opts.release ? oneOf2(opts.release, RELEASES, "--release") : c.creditsRelease ?? "upfront";
|
|
7463
|
+
const perMilestone = release === "milestones";
|
|
7464
|
+
let milestones;
|
|
7465
|
+
let currency = String(opts.currency || "USD").toUpperCase();
|
|
7466
|
+
if (opts.payment) {
|
|
7467
|
+
milestones = opts.payment.map((spec) => parsePayment(spec, perMilestone));
|
|
7468
|
+
if (milestones.length < 2 || milestones.length > 3) throw new Error("A payment plan has 2 or 3 payments");
|
|
7469
|
+
} else {
|
|
7470
|
+
currency = "USD";
|
|
7471
|
+
milestones = c.milestones.map((m) => ({
|
|
7472
|
+
key: m.key,
|
|
7473
|
+
kind: m.kind,
|
|
7474
|
+
title: m.title,
|
|
7475
|
+
deliverable: m.deliverable,
|
|
7476
|
+
dueDate: m.dueDate,
|
|
7477
|
+
cash: m.cashUsd,
|
|
7478
|
+
...perMilestone ? { credits: m.credits } : {}
|
|
7479
|
+
}));
|
|
7480
|
+
}
|
|
7481
|
+
const body = { currency, creditsRelease: release, milestones };
|
|
7482
|
+
if (!perMilestone) body.credits = opts.credits !== void 0 ? Math.round(Number(opts.credits)) : c.credits;
|
|
7483
|
+
const updated = await client.put(`${CONTRACTS}/${c.id}/plan`, body);
|
|
7484
|
+
render(updated, (u) => {
|
|
7485
|
+
success(`Sent plan v${u.planVersion}: ${usd2(u.totalUsd)} (${Math.round(u.creditsShare * 100)}% credits)`);
|
|
7486
|
+
info(color.dim(`Next: ${nextStep(u)}`));
|
|
7487
|
+
});
|
|
7488
|
+
});
|
|
7489
|
+
ct.command("accept <contract>").description("Accept the other side's plan (locks the contract)").action(async (ref) => {
|
|
7490
|
+
const client = backreelClient(globals2);
|
|
7491
|
+
const c = await loadContract(client, ref);
|
|
7492
|
+
const updated = await client.post(`${CONTRACTS}/${c.id}/accept`, { version: c.planVersion });
|
|
7493
|
+
render(updated, (u) => {
|
|
7494
|
+
success(`Accepted plan v${c.planVersion} \u2014 contract is ${u.status}`);
|
|
7495
|
+
info(color.dim(`Next: ${nextStep(u)}`));
|
|
7496
|
+
});
|
|
7497
|
+
});
|
|
7498
|
+
ct.command("fund <contract>").description("Poster: add the contract's credits from your WJI balance; creates the project workspace and starts payment 1").option("-y, --yes", "skip confirmation").action(async (ref, opts) => {
|
|
7499
|
+
const client = backreelClient(globals2);
|
|
7500
|
+
const c = await loadContract(client, ref);
|
|
7501
|
+
if (c.myCredits != null && c.myCredits < c.credits) {
|
|
7502
|
+
throw new Error(`You have ${fmt2(c.myCredits)} general credits; this contract needs ${fmt2(c.credits)}. Top up on WJI first.`);
|
|
7503
|
+
}
|
|
7504
|
+
if (!await confirm(`Add ${fmt2(c.credits)} credits to "${c.job?.title}" and start production?`, opts.yes)) return info("Aborted");
|
|
7505
|
+
await run(c.id, "/fund", void 0, (u) => `Funded \u2014 project workspace ${u.escrow?.workspaceId}`);
|
|
7506
|
+
});
|
|
7507
|
+
ct.command("submit <contract> <payment>").description("Creator: submit a payment (milestone) for review").option("--note <text|@file>", "what you are delivering").option("--link <url...>", "links to the delivery (max 5)").action(async (ref, n, opts) => {
|
|
7508
|
+
const client = backreelClient(globals2);
|
|
7509
|
+
const c = await loadContract(client, ref);
|
|
7510
|
+
const note = opts.note ? await readValue(opts.note) : "";
|
|
7511
|
+
if (!note && !opts.link) throw new Error("Pass --note and/or --link");
|
|
7512
|
+
await run(c.id, `/milestones/${milestoneKey(c, n)}/submit`, { note, links: opts.link || [] }, () => `Submitted payment ${n}`);
|
|
7513
|
+
});
|
|
7514
|
+
for (const action of ["approve", "revise"]) {
|
|
7515
|
+
ct.command(`${action} <contract> <payment>`).description(action === "approve" ? "Poster: approve a submitted payment (starts the next one)" : "Poster: ask for a revision (2 included per payment)").option("--note <text|@file>", action === "revise" ? "what to change (required)" : "optional note").action(async (ref, n, opts) => {
|
|
7516
|
+
const client = backreelClient(globals2);
|
|
7517
|
+
const c = await loadContract(client, ref);
|
|
7518
|
+
const note = opts.note ? await readValue(opts.note) : "";
|
|
7519
|
+
if (action === "revise" && !note) throw new Error("--note is required to ask for a revision");
|
|
7520
|
+
await run(
|
|
7521
|
+
c.id,
|
|
7522
|
+
`/milestones/${milestoneKey(c, n)}/${action}`,
|
|
7523
|
+
{ note },
|
|
7524
|
+
(u) => action === "approve" ? u.status === "completed" ? "Approved \u2014 contract completed" : `Approved payment ${n}` : `Asked for a revision on payment ${n}`
|
|
7525
|
+
);
|
|
7526
|
+
});
|
|
7527
|
+
}
|
|
7528
|
+
ct.command("paid <contract> <payment>").description("Poster: mark the cash for an approved payment as paid").action(async (ref, n) => {
|
|
7529
|
+
const client = backreelClient(globals2);
|
|
7530
|
+
const c = await loadContract(client, ref);
|
|
7531
|
+
await run(c.id, `/milestones/${milestoneKey(c, n)}/cash-paid`, void 0, () => `Marked payment ${n} as paid`);
|
|
7532
|
+
});
|
|
7533
|
+
ct.command("received <contract> <payment>").description("Creator: confirm the cash for a payment arrived").action(async (ref, n) => {
|
|
7534
|
+
const client = backreelClient(globals2);
|
|
7535
|
+
const c = await loadContract(client, ref);
|
|
7536
|
+
await run(c.id, `/milestones/${milestoneKey(c, n)}/cash-received`, void 0, () => `Confirmed payment ${n} received`);
|
|
7537
|
+
});
|
|
7538
|
+
ct.command("cancel <contract>").description("Cancel (planning / funding), ask to cancel (production), or confirm the other side\u2019s request").option("--reason <text>", "reason").option("--withdraw", "withdraw your own cancel request").option("--decline", "decline the other side's cancel request").option("-y, --yes", "skip confirmation").action(async (ref, opts) => {
|
|
7539
|
+
if (opts.withdraw) return void await run(ref, "/cancel/withdraw", void 0, () => "Cancel request withdrawn");
|
|
7540
|
+
if (opts.decline) return void await run(ref, "/cancel/decline", void 0, () => "Declined \u2014 the contract continues");
|
|
7541
|
+
if (!await confirm(`Cancel contract ${ref}?`, opts.yes)) return info("Aborted");
|
|
7542
|
+
await run(
|
|
7543
|
+
ref,
|
|
7544
|
+
"/cancel",
|
|
7545
|
+
{ reason: opts.reason || "" },
|
|
7546
|
+
(u) => u.status === "cancelled" ? "Contract cancelled" : "Cancel request sent \u2014 the other side has to confirm"
|
|
7547
|
+
);
|
|
7548
|
+
});
|
|
7549
|
+
ct.command("topup <contract> <credits>").description("Creator: add your own credits to the project (unused ones come back to you)").action(async (ref, credits) => {
|
|
7550
|
+
const n = Math.floor(Number(credits));
|
|
7551
|
+
if (!(n > 0)) throw new Error("credits must be a positive number");
|
|
7552
|
+
await run(ref, "/topup", { credits: n }, (u) => `Topped up ${fmt2(n)} \u2014 ${fmt2(Math.max(0, u.escrow?.left ?? 0))} project credits left`);
|
|
7553
|
+
});
|
|
7554
|
+
const change = ct.command("change").description("Change requests: ask for more credits / cash when scope changes or extra revisions");
|
|
7555
|
+
change.command("request <contract>", { isDefault: true }).description("Creator: propose extra credits and/or cash").option("--credits <n>", "extra credits", "0").option("--cash <n>", "extra cash in USD").option("--reason <r>", `${CR_REASONS.join(", ")}`, "scope").option("--payment <n>", "the payment it relates to (for extra revisions)").requiredOption("--note <text|@file>", "what changed").action(async (ref, opts) => {
|
|
7556
|
+
const client = backreelClient(globals2);
|
|
7557
|
+
const c = await loadContract(client, ref);
|
|
7558
|
+
const body = {
|
|
7559
|
+
reason: oneOf2(opts.reason, CR_REASONS, "--reason"),
|
|
7560
|
+
note: await readValue(opts.note),
|
|
7561
|
+
credits: Math.round(Number(opts.credits) || 0),
|
|
7562
|
+
cashUsd: opts.cash ? Number(opts.cash) : 0
|
|
7563
|
+
};
|
|
7564
|
+
if (opts.payment) body.milestoneKey = milestoneKey(c, opts.payment);
|
|
7565
|
+
const cashUsd = Number(body.cashUsd);
|
|
7566
|
+
const min = cashUsd > 0 ? Math.ceil(cashUsd * c.creditsPerUsd * 0.25 / 0.75) : 0;
|
|
7567
|
+
if (Number(body.credits) < min) throw new Error(`At least 25% of the extra must be credits: --credits ${min} or more`);
|
|
7568
|
+
await run(c.id, "/change-requests", body, () => "Change request sent to the poster");
|
|
7569
|
+
});
|
|
7570
|
+
for (const action of ["accept", "decline", "withdraw"]) {
|
|
7571
|
+
change.command(`${action} <contract> [requestId]`).description(
|
|
7572
|
+
action === "accept" ? "Poster: accept and add the extra credits from your WJI balance" : action === "decline" ? "Poster: decline" : "Creator: withdraw your request"
|
|
7573
|
+
).option("--note <text>", "reply to the other side").action(async (ref, requestId, opts) => {
|
|
7574
|
+
const client = backreelClient(globals2);
|
|
7575
|
+
const c = await loadContract(client, ref);
|
|
7576
|
+
const id = requestId || (c.changeRequests || []).find((r) => r.status === "pending")?.id;
|
|
7577
|
+
if (!id) throw new Error("No pending change request");
|
|
7578
|
+
await run(
|
|
7579
|
+
c.id,
|
|
7580
|
+
`/change-requests/${id}/${action}`,
|
|
7581
|
+
action === "withdraw" ? void 0 : { note: opts.note || "" },
|
|
7582
|
+
() => action === "accept" ? "Accepted \u2014 contract total updated" : action === "decline" ? "Declined" : "Withdrawn"
|
|
7583
|
+
);
|
|
7584
|
+
});
|
|
7585
|
+
}
|
|
7586
|
+
ct.command("dispute <contract>").description("Report a problem: generation in the project workspace stops until a WJI admin decides").option("--reason <text|@file>", "what went wrong (required to open)").option("--withdraw", "withdraw the dispute you raised").option("-y, --yes", "skip confirmation").action(async (ref, opts) => {
|
|
7587
|
+
if (opts.withdraw) return void await run(ref, "/dispute/withdraw", void 0, () => "Dispute withdrawn \u2014 back in production");
|
|
7588
|
+
if (!opts.reason) throw new Error("--reason is required");
|
|
7589
|
+
const reason = await readValue(opts.reason);
|
|
7590
|
+
if (!await confirm("Open a dispute? Generation in the project workspace pauses until an admin decides.", opts.yes)) return info("Aborted");
|
|
7591
|
+
await run(ref, "/dispute", { reason }, () => "Dispute opened \u2014 add your side with: wji br contract statement");
|
|
7592
|
+
});
|
|
7593
|
+
ct.command("statement <contract>").description("Add a statement to the open dispute").requiredOption("--text <text|@file>", "your side of the story").option("--link <url...>", "supporting links (max 5)").action(async (ref, opts) => {
|
|
7594
|
+
await run(ref, "/dispute/statement", { text: await readValue(opts.text), links: opts.link || [] }, () => "Statement added");
|
|
7595
|
+
});
|
|
7596
|
+
return ct;
|
|
7597
|
+
}
|
|
7598
|
+
|
|
7599
|
+
// src/commands/backreel-profile.ts
|
|
7600
|
+
import { Command as Command25 } from "commander";
|
|
7601
|
+
function orgCommand2(globals2) {
|
|
7602
|
+
const org = new Command25("org").aliases(["orgs"]).description("Your organizations on Backreel (post, bid and publish as one with --org)");
|
|
7603
|
+
org.command("list", { isDefault: true }).description("Organizations you belong to, with your role").action(async () => {
|
|
7604
|
+
const client = backreelClient(globals2);
|
|
7605
|
+
const rows = await client.getFresh("/backreel/org/mine");
|
|
7606
|
+
render(
|
|
7607
|
+
rows,
|
|
7608
|
+
(r) => table(r || [], [
|
|
7609
|
+
{ header: "id", value: (o) => o.id },
|
|
7610
|
+
{ header: "name", value: (o) => o.name },
|
|
7611
|
+
{ header: "role", value: (o) => o.role }
|
|
7612
|
+
])
|
|
7613
|
+
);
|
|
7614
|
+
});
|
|
7615
|
+
org.command("get <orgId>").description("An organization's public page: about, region, members, verifications").action(async (id) => {
|
|
7616
|
+
const client = backreelClient(globals2);
|
|
7617
|
+
const o = await client.getFresh(`/backreel/org/${id}`);
|
|
7618
|
+
render(
|
|
7619
|
+
o,
|
|
7620
|
+
() => detail([
|
|
7621
|
+
["id", o.id],
|
|
7622
|
+
["name", o.name],
|
|
7623
|
+
["region", o.region || color.dim("\u2014")],
|
|
7624
|
+
["members", o.memberCount],
|
|
7625
|
+
["you", o.member ? o.member.role ?? "member" : color.dim("not a member")],
|
|
7626
|
+
["verified as", (o.verifications || []).map((v) => v.kind ?? v).join(", ") || color.dim("\u2014")],
|
|
7627
|
+
["about", o.description]
|
|
7628
|
+
])
|
|
7629
|
+
);
|
|
7630
|
+
});
|
|
7631
|
+
return org;
|
|
7632
|
+
}
|
|
7633
|
+
function aboutCommand(globals2) {
|
|
7634
|
+
return new Command25("about").description("Set the About text and region on your Backreel profile (or an organization you manage)").option("--text <text|@file>", 'About text; "" clears it').option("--region <code>", 'ISO country / region code, e.g. HK, TW, JP; "" clears it').option("--org <orgId>", "set the organization's page instead (owner / admin)").action(async (opts) => {
|
|
7635
|
+
const body = {};
|
|
7636
|
+
if (opts.text !== void 0) body.description = opts.text === "" ? "" : await readValue(opts.text);
|
|
7637
|
+
if (opts.region !== void 0) body.region = String(opts.region).trim().toUpperCase();
|
|
7638
|
+
if (!Object.keys(body).length) throw new Error("Pass --text and/or --region");
|
|
7639
|
+
const client = backreelClient(globals2);
|
|
7640
|
+
const saved = await client.put(opts.org ? `/backreel/about/org/${opts.org}` : "/backreel/about/me", body);
|
|
7641
|
+
render(saved, () => success(`Updated ${opts.org ? "organization" : "profile"}: ${Object.keys(body).join(", ")}`));
|
|
7642
|
+
});
|
|
7643
|
+
}
|
|
7644
|
+
function historyCommand2(globals2) {
|
|
7645
|
+
const history = new Command25("history").description("Your viewing history (works and blog posts)");
|
|
7646
|
+
history.command("list", { isDefault: true }).option("--type <t>", "post or blog").option("--page <n>", "page", "1").option("--limit <n>", "page size (\u226450)", "20").action(async (opts) => {
|
|
7647
|
+
const client = backreelClient(globals2);
|
|
7648
|
+
const result = await client.getFresh("/backreel/view/history", {
|
|
7649
|
+
page: Number(opts.page),
|
|
7650
|
+
limit: Number(opts.limit),
|
|
7651
|
+
type: opts.type
|
|
7652
|
+
});
|
|
7653
|
+
render(result, (r) => {
|
|
7654
|
+
table(r?.data || [], [
|
|
7655
|
+
{ header: "id", value: (h) => h.id },
|
|
7656
|
+
{ header: "type", value: (h) => h.targetType },
|
|
7657
|
+
{ header: "title", value: (h) => h.item ? truncate(h.item.title, 36) : color.dim("(no longer available)") },
|
|
7658
|
+
{ header: "views", value: (h) => h.count },
|
|
7659
|
+
{ header: "last", value: (h) => relativeTime(h.lastViewedAt) }
|
|
7660
|
+
]);
|
|
7661
|
+
const p = r?.pagination;
|
|
7662
|
+
if (p) info(color.dim(`${p.total} total \xB7 page ${p.page}${p.hasNext ? ` \xB7 more: --page ${p.page + 1}` : ""}`));
|
|
7663
|
+
});
|
|
7664
|
+
});
|
|
7665
|
+
history.command("rm <id>").description("Remove one entry").action(async (id) => {
|
|
7666
|
+
const client = backreelClient(globals2);
|
|
7667
|
+
const r = await client.delete(`/backreel/view/history/${id}`);
|
|
7668
|
+
render(r, (x) => success(x?.removed ? "Removed" : "Nothing to remove"));
|
|
7669
|
+
});
|
|
7670
|
+
history.command("clear").description("Clear your history").option("--type <t>", "only post or blog entries").option("-y, --yes", "skip confirmation").action(async (opts) => {
|
|
7671
|
+
if (!await confirm("Clear your Backreel viewing history?", opts.yes)) return info("Aborted");
|
|
7672
|
+
const client = backreelClient(globals2);
|
|
7673
|
+
const r = await client.delete("/backreel/view/history", opts.type ? { type: opts.type } : void 0);
|
|
7674
|
+
render(r, (x) => success(`Removed ${x?.removed ?? 0} entries`));
|
|
7675
|
+
});
|
|
7676
|
+
return history;
|
|
7677
|
+
}
|
|
7678
|
+
|
|
6811
7679
|
// src/commands/backreel.ts
|
|
6812
7680
|
var KINDS = ["showcase", "crowdfunding"];
|
|
6813
7681
|
var PROJECT_STATUSES = ["planning", "pre_production", "in_production", "post_production", "completed"];
|
|
@@ -6817,11 +7685,12 @@ var LOGLINE_MAX = 200;
|
|
|
6817
7685
|
var VERIFICATION_KINDS = ["investor", "production_company", "brand", "distributor", "sales_agent"];
|
|
6818
7686
|
var CREDIT_FIELDS = ["kind", "name", "role", "user", "photo", "avatarUrl", "link", "source", "npcId", "order"];
|
|
6819
7687
|
function backreelCommand(globals2) {
|
|
6820
|
-
const br = new
|
|
6821
|
-
br.command("list", { isDefault: true }).description("Your posts, drafts included; --all for everyone's published posts").option("--all", "search all published posts instead of yours").option("-k, --keyword <text>", "search text").option("--page <n>", "page number", "1").option("--limit <n>", "page size", "20").action(async (opts) => {
|
|
7688
|
+
const br = new Command26("backreel").aliases(["br"]).description("Manage your Backreel posts (works) and their credits, including NPC cast");
|
|
7689
|
+
br.command("list", { isDefault: true }).description("Your posts, drafts included; --all for everyone's published posts").option("--all", "search all published posts instead of yours").option("--org <orgId>", "your organization's posts, drafts included").option("-k, --keyword <text>", "search text").option("--page <n>", "page number", "1").option("--limit <n>", "page size", "20").action(async (opts) => {
|
|
6822
7690
|
const client = backreelClient(globals2);
|
|
6823
7691
|
const result = await client.get(`${POSTS}/${opts.all ? "search" : "search/my"}`, {
|
|
6824
7692
|
keyword: opts.keyword,
|
|
7693
|
+
organization: opts.org,
|
|
6825
7694
|
page: Number(opts.page),
|
|
6826
7695
|
limit: Number(opts.limit)
|
|
6827
7696
|
});
|
|
@@ -6990,7 +7859,13 @@ function backreelCommand(globals2) {
|
|
|
6990
7859
|
});
|
|
6991
7860
|
br.addCommand(episodeCommand(globals2));
|
|
6992
7861
|
br.addCommand(enquiryCommand(globals2));
|
|
7862
|
+
br.addCommand(blogCommand(globals2));
|
|
6993
7863
|
br.addCommand(fundCommand(globals2));
|
|
7864
|
+
br.addCommand(jobCommand(globals2));
|
|
7865
|
+
br.addCommand(contractCommand(globals2));
|
|
7866
|
+
br.addCommand(orgCommand2(globals2));
|
|
7867
|
+
br.addCommand(aboutCommand(globals2));
|
|
7868
|
+
br.addCommand(historyCommand2(globals2));
|
|
6994
7869
|
const credit = br.command("credit").aliases(["credits"]).description("Cast and crew of a post (max 30)");
|
|
6995
7870
|
credit.command("list <postId>", { isDefault: true }).description("Numbered credits").action(async (id) => {
|
|
6996
7871
|
const client = backreelClient(globals2);
|
|
@@ -7049,7 +7924,7 @@ function backreelCommand(globals2) {
|
|
|
7049
7924
|
return br;
|
|
7050
7925
|
}
|
|
7051
7926
|
function postOptions(cmd) {
|
|
7052
|
-
return cmd.option("--kind <kind>", KINDS.join(" | ")).option("--logline <text>", `one-line hook, max ${LOGLINE_MAX} chars; public even when the description is locked ("" clears it)`).option("--category <category>", "category").option("--project-status <status>", PROJECT_STATUSES.join(" | ")).option("--tags <tags>", "comma-separated tags (replaces the list)").option("-d, --description <text>", "description, plain text or HTML (@file / - supported)").option("--cover <assetId>", "landscape cover asset").option("--cover-portrait <assetId>", "portrait cover asset").option("--trailer <assetId>", "trailer video asset (\u226430 s)").option("--media <assetId...>", `LookBook gallery, images or videos in order (replaces it; \u2264${MEDIA_MAX}; "none" clears)`).option("--body <json>", "any other post fields as JSON (@file / - supported); flags win");
|
|
7927
|
+
return cmd.option("--kind <kind>", KINDS.join(" | ")).option("--logline <text>", `one-line hook, max ${LOGLINE_MAX} chars; public even when the description is locked ("" clears it)`).option("--category <category>", "category").option("--project-status <status>", PROJECT_STATUSES.join(" | ")).option("--tags <tags>", "comma-separated tags (replaces the list)").option("-d, --description <text>", "description, plain text or HTML (@file / - supported)").option("--cover <assetId>", "landscape cover asset").option("--cover-portrait <assetId>", "portrait cover asset").option("--trailer <assetId>", "trailer video asset (\u226430 s)").option("--media <assetId...>", `LookBook gallery, images or videos in order (replaces it; \u2264${MEDIA_MAX}; "none" clears)`).option("--org <orgId|none>", "publish as one of your organizations; none = yourself (only the author can change it later)").option("--body <json>", "any other post fields as JSON (@file / - supported); flags win");
|
|
7053
7928
|
}
|
|
7054
7929
|
async function postBody(opts) {
|
|
7055
7930
|
const body = { ...await readJson(opts.body) || {} };
|
|
@@ -7060,6 +7935,7 @@ async function postBody(opts) {
|
|
|
7060
7935
|
body.logline = logline;
|
|
7061
7936
|
}
|
|
7062
7937
|
if (opts.category) body.category = opts.category;
|
|
7938
|
+
if (opts.org !== void 0) body.organization = opts.org === "none" ? null : opts.org;
|
|
7063
7939
|
if (opts.projectStatus) body.projectStatus = oneOf2(opts.projectStatus, PROJECT_STATUSES, "--project-status");
|
|
7064
7940
|
if (opts.tags !== void 0) body.tags = String(opts.tags).split(",").map((t) => t.trim()).filter(Boolean);
|
|
7065
7941
|
if (opts.description !== void 0) body.description = toHtml(await readValue(opts.description));
|
|
@@ -7130,7 +8006,7 @@ process.stdout.on("error", (err) => {
|
|
|
7130
8006
|
if (err.code === "EPIPE") process.exit(0);
|
|
7131
8007
|
throw err;
|
|
7132
8008
|
});
|
|
7133
|
-
var program = new
|
|
8009
|
+
var program = new Command27();
|
|
7134
8010
|
program.name("wji").description("Command line interface for the WJI AI platform").version(VERSION, "-v, --version").option("--profile <name>", "config profile to use").option("--endpoint <url>", "API base URL").option("--token <token>", "bearer token or sk_ API key for this call").option("--org <orgId>", "organization context (X-Org-Id)").option("--no-org", "force personal context").option("--locale <locale>", "response language (cht, chs, en)").option("--json", "print JSON instead of tables (standard detail unless --detail is given)").option(
|
|
7135
8011
|
"--detail <level>",
|
|
7136
8012
|
"how much to return: brief | standard | full (default standard for --json; env WJI_DETAIL)"
|