@wji.ai/cli 0.8.3 → 0.9.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command23 } from "commander";
4
+ import { Command as Command27 } from "commander";
5
5
 
6
6
  // src/app.ts
7
7
  import { createRequire } from "module";
@@ -281,6 +281,8 @@ var MIME_TYPES = {
281
281
  mp3: "audio/mpeg",
282
282
  wav: "audio/wav",
283
283
  m4a: "audio/mp4",
284
+ srt: "application/x-subrip",
285
+ vtt: "text/vtt",
284
286
  pdf: "application/pdf",
285
287
  csv: "text/csv",
286
288
  txt: "text/plain",
@@ -490,6 +492,89 @@ var ApiClient = class {
490
492
  }
491
493
  };
492
494
 
495
+ // ../core/src/markup.ts
496
+ function escapeHtml(value) {
497
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
498
+ }
499
+ function safeUrl(url, opts) {
500
+ const trimmed = url.trim();
501
+ if (/^(https?:|mailto:)/i.test(trimmed)) return trimmed;
502
+ if (/^\/(?!\/)/.test(trimmed) && opts.siteBase) {
503
+ return `${opts.siteBase.replace(/\/$/, "")}${trimmed}`;
504
+ }
505
+ return "";
506
+ }
507
+ function inline(text, opts) {
508
+ let html = escapeHtml(text);
509
+ html = html.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_, alt, url) => {
510
+ const src = safeUrl(url, opts);
511
+ return src ? `<img src="${src}" alt="${alt}">` : "";
512
+ });
513
+ html = html.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => {
514
+ const href = safeUrl(url, opts);
515
+ return href ? `<a href="${href}">${label}</a>` : label;
516
+ });
517
+ html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
518
+ html = html.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
519
+ return html;
520
+ }
521
+ function markupToHtml(source, opts = {}) {
522
+ const inl = (text) => inline(text, opts);
523
+ const blocks = source.replace(/\r\n?/g, "\n").split(/\n{2,}/);
524
+ const out = [];
525
+ for (const raw of blocks) {
526
+ const block = raw.trim();
527
+ if (!block) continue;
528
+ const lines = block.split("\n");
529
+ const heading = /^(#{2,4})\s+(.*)$/.exec(block);
530
+ if (heading && lines.length === 1) {
531
+ const level = Math.min(heading[1].length, 4);
532
+ out.push(`<h${level}>${inl(heading[2])}</h${level}>`);
533
+ } else if (lines.every((l) => /^\s*[-*]\s+/.test(l))) {
534
+ out.push(`<ul>${lines.map((l) => `<li>${inl(l.replace(/^\s*[-*]\s+/, ""))}</li>`).join("")}</ul>`);
535
+ } else if (lines.every((l) => /^\s*\d+[.)]\s+/.test(l))) {
536
+ out.push(`<ol>${lines.map((l) => `<li>${inl(l.replace(/^\s*\d+[.)]\s+/, ""))}</li>`).join("")}</ol>`);
537
+ } else if (lines.every((l) => /^>\s?/.test(l))) {
538
+ out.push(`<blockquote><p>${lines.map((l) => inl(l.replace(/^>\s?/, ""))).join("<br>")}</p></blockquote>`);
539
+ } else {
540
+ out.push(`<p>${lines.map(inl).join("<br>")}</p>`);
541
+ }
542
+ }
543
+ return out.join("");
544
+ }
545
+ function decode(value) {
546
+ return value.replaceAll("&nbsp;", " ").replaceAll("&quot;", '"').replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
547
+ }
548
+ function attr(attrs, name) {
549
+ const m = new RegExp(`${name}\\s*=\\s*"([^"]*)"`, "i").exec(attrs);
550
+ return m ? decode(m[1]) : "";
551
+ }
552
+ function inlineBack(html) {
553
+ return decode(
554
+ html.replace(/<br\s*\/?>/gi, "\n").replace(/<img\b([^>]*)>/gi, (_, a) => `![${attr(a, "alt")}](${attr(a, "src")})`).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, "")
555
+ );
556
+ }
557
+ function htmlToMarkup(html) {
558
+ const out = [];
559
+ const re = /<(h[2-4]|p|ul|ol|blockquote)\b[^>]*>([\s\S]*?)<\/\1>|<img\b[^>]*>/gi;
560
+ for (const m of html.matchAll(re)) {
561
+ const [full, tag, inner = ""] = m;
562
+ if (!tag) {
563
+ out.push(inlineBack(full));
564
+ continue;
565
+ }
566
+ const name = tag.toLowerCase();
567
+ if (name.startsWith("h")) out.push(`${"#".repeat(Number(name[1]))} ${inlineBack(inner)}`);
568
+ else if (name === "ul" || name === "ol") {
569
+ const items = [...inner.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map((li) => inlineBack(li[1]));
570
+ out.push(items.map((it, i) => name === "ul" ? `- ${it}` : `${i + 1}. ${it}`).join("\n"));
571
+ } else if (name === "blockquote") {
572
+ out.push(inlineBack(inner.replace(/<\/?p>/gi, "")).split("\n").map((l) => `> ${l}`).join("\n"));
573
+ } else out.push(inlineBack(inner));
574
+ }
575
+ return out.join("\n\n");
576
+ }
577
+
493
578
  // ../core/src/pool.ts
494
579
  async function mapLimit(items, limit, fn) {
495
580
  const out = new Array(items.length);
@@ -5775,7 +5860,7 @@ function npcCommand(globals2) {
5775
5860
  const result = await client.get(`${RESIDENTS}/handle-available`, { handle, excludeId });
5776
5861
  render(
5777
5862
  result,
5778
- (r) => r.available ? success(`${r.handle} is available`) : warn(`${r.handle} is not available (${r.reason})`)
5863
+ (r) => r.available ? success(`${r.handle} is available`) : warn(`${r.handle} is not available: ${handleReason(r.reason)}`)
5779
5864
  );
5780
5865
  });
5781
5866
  const job = npc.command("job").aliases(["jobs"]).description("An NPC's work history: concerts, releases, ads\u2026");
@@ -5929,7 +6014,7 @@ function npcCommand(globals2) {
5929
6014
  return npc;
5930
6015
  }
5931
6016
  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");
6017
+ 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
6018
  }
5934
6019
  function jobOptions(cmd) {
5935
6020
  return cmd.option("--about <text>", "what happened (the job's detail line)").option("--location <text>", "where").option("--url <url>", "link");
@@ -5959,7 +6044,13 @@ async function imageUrl(client, value) {
5959
6044
  }
5960
6045
  async function assertHandle(client, handle, excludeId) {
5961
6046
  const r = await client.get(`${RESIDENTS}/handle-available`, { handle, excludeId });
5962
- if (!r.available) throw new Error(`Handle ${r.handle || handle} is not available (${r.reason})`);
6047
+ if (!r.available) throw new Error(`Handle ${r.handle || handle} is not available: ${handleReason(r.reason)}`);
6048
+ }
6049
+ function handleReason(reason) {
6050
+ if (reason === "taken") return "another NPC already uses it";
6051
+ if (reason === "reserved") return "reserved by NPC City (system / brand names like admin, official, npccity, wji, backreel)";
6052
+ if (reason === "invalid") return "use 2\u201332 of a-z 0-9 . _ -";
6053
+ return reason || "unknown reason";
5963
6054
  }
5964
6055
  function oneOf(value, allowed, flag) {
5965
6056
  if (!allowed.includes(value)) throw new Error(`${flag} must be one of ${allowed.join(", ")}`);
@@ -6015,7 +6106,7 @@ function printChanges2(changes, warnings = []) {
6015
6106
  }
6016
6107
 
6017
6108
  // src/commands/backreel.ts
6018
- import { Command as Command22 } from "commander";
6109
+ import { Command as Command26 } from "commander";
6019
6110
 
6020
6111
  // src/lib/backreel.ts
6021
6112
  var POSTS = "/backreel/post";
@@ -6259,8 +6350,9 @@ async function uploadToVod(token, file, onProgress) {
6259
6350
  var MAX_EPISODES = 30;
6260
6351
  var VIDEO_EXTS = [".mp4", ".mov", ".m4v", ".webm"];
6261
6352
  var ASSET_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
6353
+ var subtitlesOf = (e) => (e.subtitles || []).map((s) => ({ id: s.id, lang: s.lang, ...s.label ? { label: s.label } : {} }));
6262
6354
  function toDto(e, order) {
6263
- const base = { title: e.title, order, tier: e.tier || "public" };
6355
+ const base = { title: e.title, order, tier: e.tier || "public", subtitles: subtitlesOf(e) };
6264
6356
  return e.vodVid ? { ...base, vodVid: e.vodVid } : { ...base, video: idOf2(e.video) };
6265
6357
  }
6266
6358
  async function loadEpisodes(client, postId) {
@@ -6304,9 +6396,62 @@ function printEpisodes(post) {
6304
6396
  { header: "tier", value: (e) => e.tier },
6305
6397
  { header: "source", value: (e) => e.vodVid ? `vod ${e.vodVid}` : e.video ? `asset ${idOf2(e.video)}` : "-" },
6306
6398
  { header: "status", value: (e) => e.vodVid ? e.vodStatus || "?" : e.video ? "ready" : "" },
6307
- { header: "duration", value: (e) => e.durationSec ? `${Math.round(e.durationSec)}s` : "" }
6399
+ { header: "duration", value: (e) => e.durationSec ? `${Math.round(e.durationSec)}s` : "" },
6400
+ { header: "subs", value: (e) => (e.subtitles || []).map((s) => s.lang).join(" ") }
6308
6401
  ]);
6309
6402
  }
6403
+ var MAX_SUBTITLES = 8;
6404
+ var SUBTITLE_EXTS = [".srt", ".vtt"];
6405
+ var LANG_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8}){0,3}$/;
6406
+ function guessSubtitleLang(fileName) {
6407
+ const padded = `.${basename6(fileName).toLowerCase().replace(/\.(srt|vtt)$/, "")}.`;
6408
+ const hints = [
6409
+ [/(zh[-_]?(hant|tw|hk)|繁|cht)/, "zh-Hant"],
6410
+ [/(zh[-_]?(hans|cn)|简|chs)/, "zh-Hans"],
6411
+ [/(yue|粵|粤)/, "yue"],
6412
+ [/[._ -](en|eng|english)[._ -]/, "en"],
6413
+ [/[._ -](ja|jp|jpn)[._ -]/, "ja"],
6414
+ [/[._ -](ko|kr|kor)[._ -]/, "ko"],
6415
+ [/[._ -](th|tha)[._ -]/, "th"],
6416
+ [/[._ -](vi|vie)[._ -]/, "vi"]
6417
+ ];
6418
+ return hints.find(([re]) => re.test(padded))?.[1];
6419
+ }
6420
+ function printSubtitles(episode) {
6421
+ const subs = episode.subtitles || [];
6422
+ if (!subs.length) return info(`Episode ${(episode.order ?? 0) + 1} has no subtitles`);
6423
+ table(subs, [
6424
+ { header: "lang", value: (s) => s.lang },
6425
+ { header: "label", value: (s) => s.label || "" },
6426
+ { header: "default", value: (s) => s === subs[0] ? color.green("\u2605") : "" },
6427
+ { header: "id", value: (s) => color.dim(s.id) }
6428
+ ]);
6429
+ }
6430
+ async function saveSubtitles(client, postId, episodes, i, message) {
6431
+ const updated = await saveEpisodes(client, postId, episodes);
6432
+ const episode = [...updated.episodes || []].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))[i];
6433
+ render(episode?.subtitles || [], () => {
6434
+ success(message);
6435
+ printSubtitles(episode);
6436
+ });
6437
+ }
6438
+ function findTrack(subs, lang, i) {
6439
+ const at = subs.findIndex((s) => s.lang.toLowerCase() === lang.toLowerCase());
6440
+ if (at < 0) {
6441
+ const have = subs.map((s) => s.lang).join(", ") || "none";
6442
+ throw new Error(`Episode ${i + 1} has no ${lang} subtitles (has: ${have})`);
6443
+ }
6444
+ return at;
6445
+ }
6446
+ function pickRendition(renditions, want) {
6447
+ const w = want.toLowerCase();
6448
+ const hit = renditions.find((r) => String(r.definition || "").toLowerCase() === w) || renditions.find((r) => r.height && String(r.height) === w.replace(/p$/, ""));
6449
+ if (!hit) {
6450
+ const have = renditions.map((r) => r.definition || `${r.height}p`).join(", ") || "only the default";
6451
+ throw new Error(`No ${want} rendition (available: ${have})`);
6452
+ }
6453
+ return hit;
6454
+ }
6310
6455
  var sourceOptions = (cmd) => cmd.option("--vid <vid>", "a Vid already in the Backreel VOD space").option("--asset <assetId>", "an uploaded WJI video asset (legacy path, public URL)").option("--legacy", "upload the file as a WJI asset (\u2264200 MB) instead of VOD");
6311
6456
  function episodeCommand(globals2) {
6312
6457
  const ep = new Command19("episode").aliases(["episodes", "ep"]).description("Episodes of a showcase post: upload, reorder, gate by tier, check transcoding");
@@ -6328,7 +6473,7 @@ function episodeCommand(globals2) {
6328
6473
  const at = opts.at === void 0 ? episodes.length : episodeIndex(opts.at, episodes.length + 1);
6329
6474
  const source = await resolveSource(client, opts, postId, file);
6330
6475
  if (!source) throw new Error("Give the video: a file, --vid <vid> or --asset <assetId>");
6331
- episodes.splice(at, 0, { title: opts.title, order: at, tier, ...source });
6476
+ episodes.splice(at, 0, { title: opts.title, order: at, tier, subtitles: [], ...source });
6332
6477
  const updated = await saveEpisodes(client, postId, episodes);
6333
6478
  render(updated.episodes, () => {
6334
6479
  success(`Added episode ${at + 1} "${opts.title}" to "${updated.title}"`);
@@ -6344,12 +6489,21 @@ function episodeCommand(globals2) {
6344
6489
  const i = episodeIndex(n, episodes.length);
6345
6490
  const source = await resolveSource(client, { ...opts, title: opts.title || episodes[i].title }, postId, file);
6346
6491
  if (!opts.title && !opts.tier && !source) throw new Error("Nothing to change \u2014 pass --title, --tier or a new video");
6347
- const next = { title: opts.title || episodes[i].title, order: i, tier: opts.tier ? oneOf2(opts.tier, TIERS, "--tier") : episodes[i].tier };
6492
+ const next = {
6493
+ title: opts.title || episodes[i].title,
6494
+ order: i,
6495
+ tier: opts.tier ? oneOf2(opts.tier, TIERS, "--tier") : episodes[i].tier,
6496
+ // a new cut of the same episode usually keeps its timing, so the tracks stay
6497
+ subtitles: episodes[i].subtitles
6498
+ };
6348
6499
  episodes[i] = source ? { ...next, ...source } : { ...episodes[i], ...next };
6349
6500
  const updated = await saveEpisodes(client, postId, episodes);
6350
6501
  render(updated.episodes, () => {
6351
6502
  success(`Updated episode ${i + 1}`);
6352
6503
  printEpisodes(updated);
6504
+ if (source && next.subtitles.length) {
6505
+ info(color.dim(`Kept ${next.subtitles.length} subtitle track(s) \u2014 \`wji br episode subs rm\` any that no longer line up`));
6506
+ }
6353
6507
  });
6354
6508
  });
6355
6509
  ep.command("rm <postId> <n...>").description("Remove episodes (later ones move up)").option("-y, --yes", "skip confirmation").action(async (postId, ns, opts) => {
@@ -6424,25 +6578,251 @@ function episodeCommand(globals2) {
6424
6578
  if (rows.some((r) => r.status === "failed")) warn("A failed transcode needs a new upload: `episode set <post> <n> <file>`");
6425
6579
  });
6426
6580
  });
6427
- ep.command("play <postId> <n>").description("Print a short-lived play URL for episode n (as the owner)").action(async (postId, n) => {
6581
+ ep.command("play <postId> <n>").description("Print a short-lived play URL for episode n (as the owner) \u2014 the best quality unless --quality").option("-q, --quality <q>", "a rendition such as 720p or 480p").option("--qualities", "list every rendition instead of printing one URL").action(async (postId, n, opts) => {
6428
6582
  const client = backreelClient(globals2);
6429
6583
  const post = await loadPost(client, postId);
6430
6584
  const i = episodeIndex(n, (post.episodes || []).length);
6431
6585
  const order = [...post.episodes].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))[i].order ?? i;
6432
6586
  const play = await client.get(`${POSTS}/list/${postId}/episodes/${order}/play`);
6433
- render(play, (p) => {
6587
+ const renditions = play.renditions || [];
6588
+ if (opts.qualities) {
6589
+ return render(renditions, (rows) => {
6590
+ if (!rows.length) return info("Only one version (legacy asset, or VOD returned no renditions)");
6591
+ table(rows, [
6592
+ { header: "quality", value: (r) => r.definition || "" },
6593
+ { header: "size", value: (r) => r.width && r.height ? `${r.width}\xD7${r.height}` : "" },
6594
+ { header: "bitrate", value: (r) => r.bitrate ? `${Math.round(r.bitrate / 1e3)} kbps` : "" }
6595
+ ]);
6596
+ if (play.expiresAt) info(color.dim(`URLs expire ${play.expiresAt} \u2014 --json to get them`));
6597
+ });
6598
+ }
6599
+ const url = opts.quality ? pickRendition(renditions, opts.quality).url : play.url;
6600
+ render({ ...play, url }, (p) => {
6434
6601
  process.stdout.write(`${p.url}
6435
6602
  `);
6436
6603
  if (p.expiresAt) info(color.dim(`expires ${p.expiresAt}`));
6437
6604
  });
6438
6605
  });
6606
+ const subs = ep.command("subs").aliases(["subtitles", "sub"]).description("Subtitle tracks of an episode: .srt / .vtt, one per language, \u22648, the first is the default");
6607
+ subs.command("list <postId> <n>", { isDefault: true }).description("List episode n's subtitle tracks").action(async (postId, n) => {
6608
+ const client = backreelClient(globals2);
6609
+ const post = await loadPost(client, postId);
6610
+ const i = episodeIndex(n, (post.episodes || []).length);
6611
+ const episode = [...post.episodes].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))[i];
6612
+ render(episode.subtitles || [], () => printSubtitles(episode));
6613
+ });
6614
+ subs.command("add <postId> <n> <file>").description("Upload a .srt / .vtt and attach it to episode n").option("-l, --lang <lang>", "BCP 47 language: en, ja, ko, zh-Hant, zh-Hans, yue\u2026 (guessed from the file name if omitted)").option("--label <label>", "name shown in the player (default: the language's own name)").option("--default", "make it the default track (first in the list)").option("--replace", "replace the episode's existing track in that language").action(async (postId, n, file, opts) => {
6615
+ const client = backreelClient(globals2);
6616
+ const ext = extname2(file).toLowerCase();
6617
+ if (!SUBTITLE_EXTS.includes(ext)) throw new Error(`${basename6(file)}: subtitles must be .srt or .vtt`);
6618
+ if ((await stat5(file)).size > 1024 * 1024) throw new Error(`${basename6(file)} is over 1 MB`);
6619
+ const lang = opts.lang || guessSubtitleLang(file);
6620
+ if (!lang) throw new Error(`Can't tell the language from "${basename6(file)}" \u2014 pass --lang (e.g. en, zh-Hant)`);
6621
+ if (!LANG_RE.test(lang)) throw new Error(`--lang ${lang} isn't a language code like en, ja or zh-Hant`);
6622
+ const label = opts.label ? String(opts.label).trim() : void 0;
6623
+ if (label && label.length > 40) throw new Error("--label is at most 40 characters");
6624
+ const { post, episodes } = await loadEpisodes(client, postId);
6625
+ const i = episodeIndex(n, episodes.length);
6626
+ const duration = [...post.episodes].sort((x, y) => (x.order ?? 0) - (y.order ?? 0))[i]?.durationSec;
6627
+ const list = [...episodes[i].subtitles];
6628
+ const existing = list.findIndex((s) => s.lang.toLowerCase() === lang.toLowerCase());
6629
+ if (existing >= 0 && !opts.replace) {
6630
+ throw new Error(`Episode ${i + 1} already has ${lang} subtitles \u2014 add --replace to swap them`);
6631
+ }
6632
+ if (existing < 0 && list.length >= MAX_SUBTITLES) {
6633
+ throw new Error(`Episode ${i + 1} already has ${MAX_SUBTITLES} subtitle tracks`);
6634
+ }
6635
+ const uploaded = await client.upload("/backreel/subtitle/upload", file, {});
6636
+ const keptLabel = label ?? (existing >= 0 ? list[existing].label : void 0);
6637
+ const track = { id: uploaded.id, lang, ...keptLabel ? { label: keptLabel } : {} };
6638
+ if (existing >= 0) list.splice(existing, 1, track);
6639
+ else list.push(track);
6640
+ if (opts.default) list.unshift(...list.splice(list.indexOf(track), 1));
6641
+ episodes[i].subtitles = list;
6642
+ await saveSubtitles(client, postId, episodes, i, `${existing >= 0 ? "Replaced" : "Added"} ${lang} subtitles on episode ${i + 1} (${uploaded.cueCount} lines)`);
6643
+ if (duration && uploaded.lastCueSec > duration + 2) {
6644
+ warn(`The last line is at ${Math.round(uploaded.lastCueSec)}s but the episode is ${Math.round(duration)}s \u2014 is this the right file?`);
6645
+ }
6646
+ });
6647
+ subs.command("rm <postId> <n> <lang...>").description("Remove episode n's tracks in these languages").action(async (postId, n, langs) => {
6648
+ const client = backreelClient(globals2);
6649
+ const { episodes } = await loadEpisodes(client, postId);
6650
+ const i = episodeIndex(n, episodes.length);
6651
+ const drop = new Set(langs.map((l) => findTrack(episodes[i].subtitles, l, i)));
6652
+ episodes[i].subtitles = episodes[i].subtitles.filter((_, at) => !drop.has(at));
6653
+ await saveSubtitles(client, postId, episodes, i, `Removed ${langs.join(", ")} from episode ${i + 1}`);
6654
+ });
6655
+ subs.command("default <postId> <n> <lang>").description("Make a track the default (moves it first)").action(async (postId, n, lang) => {
6656
+ const client = backreelClient(globals2);
6657
+ const { episodes } = await loadEpisodes(client, postId);
6658
+ const i = episodeIndex(n, episodes.length);
6659
+ const list = [...episodes[i].subtitles];
6660
+ list.unshift(...list.splice(findTrack(list, lang, i), 1));
6661
+ episodes[i].subtitles = list;
6662
+ await saveSubtitles(client, postId, episodes, i, `${list[0].lang} is now episode ${i + 1}'s default`);
6663
+ });
6664
+ subs.command("label <postId> <n> <lang> [label]").description("Set the name the player shows for a track (omit it to go back to the language name)").action(async (postId, n, lang, label) => {
6665
+ const client = backreelClient(globals2);
6666
+ if (label && label.trim().length > 40) throw new Error("A label is at most 40 characters");
6667
+ const { episodes } = await loadEpisodes(client, postId);
6668
+ const i = episodeIndex(n, episodes.length);
6669
+ const at = findTrack(episodes[i].subtitles, lang, i);
6670
+ const { label: _old, ...rest } = episodes[i].subtitles[at];
6671
+ episodes[i].subtitles[at] = label?.trim() ? { ...rest, label: label.trim() } : rest;
6672
+ await saveSubtitles(client, postId, episodes, i, `Updated the ${rest.lang} label on episode ${i + 1}`);
6673
+ });
6439
6674
  return ep;
6440
6675
  }
6441
6676
 
6677
+ // src/commands/backreel-blog.ts
6678
+ import { Command as Command20 } from "commander";
6679
+ var BLOG = "/backreel/blog";
6680
+ var TAG_MAX = 8;
6681
+ async function readBody(value, asHtml) {
6682
+ if (value === void 0) return void 0;
6683
+ const text = await readValue(value);
6684
+ if (asHtml) return text;
6685
+ if (/\]\(\/(?!\/)/.test(text)) {
6686
+ info(color.yellow("! Links like (/blog) are removed by Backreel \u2014 use full https:// URLs"));
6687
+ }
6688
+ return markupToHtml(text);
6689
+ }
6690
+ var parseTags = (value) => value === void 0 ? void 0 : value.split(/[,,、]/).map((it) => it.trim()).filter(Boolean).slice(0, TAG_MAX);
6691
+ async function resolveCover(client, value) {
6692
+ if (value === void 0) return void 0;
6693
+ if (value === "none" || value === "") return null;
6694
+ if (/^[a-f0-9]{24}$/i.test(value)) return value;
6695
+ info(color.dim(`Uploading ${value}\u2026`));
6696
+ const asset = await client.upload("/assets/v2/upload", value, {});
6697
+ return asset.id;
6698
+ }
6699
+ function resolvePost(value) {
6700
+ if (value === void 0) return void 0;
6701
+ return value === "none" || value === "" ? null : value;
6702
+ }
6703
+ function printList(rows) {
6704
+ table(rows, [
6705
+ { header: "id", value: (b) => b.id },
6706
+ { header: "title", value: (b) => truncate(b.title, 34) },
6707
+ { header: "kind", value: (b) => b.kind === "official" ? color.cyan("official") : "member" },
6708
+ {
6709
+ header: "status",
6710
+ value: (b) => [b.status, b.pinned ? "pinned" : "", b.hidden ? color.yellow("hidden") : ""].filter(Boolean).join(" ")
6711
+ },
6712
+ { header: "project", value: (b) => truncate(b.post?.title, 20) },
6713
+ { header: "author", value: (b) => truncate(b.author?.name, 16) },
6714
+ { header: "published", value: (b) => relativeTime(b.publishedAt) ?? color.dim("\u2014") }
6715
+ ]);
6716
+ }
6717
+ function printBlog(b, opts) {
6718
+ detail([
6719
+ ["id", b.id],
6720
+ ["title", b.title],
6721
+ ["kind", b.kind],
6722
+ ["status", b.status],
6723
+ ["hidden", b.hidden ? `yes \u2014 ${b.hiddenReason || "no reason given"}` : void 0],
6724
+ ["pinned", b.pinned ? "yes" : void 0],
6725
+ ["project", b.post ? `${b.post.title} (${b.post.id})` : "profile"],
6726
+ ["author", b.author ? `${b.author.name} (${b.author.id})` : void 0],
6727
+ ["tags", (b.tags || []).join(", ") || void 0],
6728
+ ["cover", b.cover?.url],
6729
+ ["excerpt", b.excerpt],
6730
+ ["published", b.publishedAt],
6731
+ ["updated", b.updatedAt]
6732
+ ]);
6733
+ process.stdout.write(`
6734
+ ${opts.html ? b.body : htmlToMarkup(b.body || "")}
6735
+ `);
6736
+ }
6737
+ function writeOptions(cmd) {
6738
+ 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");
6739
+ }
6740
+ function blogCommand(globals2) {
6741
+ const blog = new Command20("blog").aliases(["blogs", "post-updates"]).description("Blog posts on your Backreel profile or under your projects");
6742
+ 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) => {
6743
+ const client = backreelClient(globals2);
6744
+ const result = await client.get(`${BLOG}/${opts.mine ? "search/my" : "search"}`, {
6745
+ kind: opts.official ? "official" : void 0,
6746
+ author: opts.author,
6747
+ organization: opts.org,
6748
+ post: opts.post,
6749
+ tag: opts.tag,
6750
+ keyword: opts.keyword,
6751
+ page: Number(opts.page),
6752
+ limit: Number(opts.limit)
6753
+ });
6754
+ render(result, (r) => {
6755
+ printList(r?.data || []);
6756
+ const p = r?.pagination;
6757
+ if (p) info(color.dim(`${p.total} total \xB7 page ${p.page}${p.hasNext ? ` \xB7 more: --page ${p.page + 1}` : ""}`));
6758
+ });
6759
+ });
6760
+ 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) => {
6761
+ const client = backreelClient(globals2);
6762
+ const b = await client.getFresh(`${BLOG}/list/${id}`);
6763
+ render(b, () => printBlog(b, { html: !!opts.html }));
6764
+ });
6765
+ writeOptions(blog.command("create")).description("Write a post (draft unless --publish)").option("--publish", "publish now").action(async (opts) => {
6766
+ if (!opts.title?.trim()) throw new Error("--title is required");
6767
+ const client = backreelClient(globals2);
6768
+ if (opts.post && opts.post !== "none") await client.getFresh(`${POSTS}/list/${opts.post}`);
6769
+ const created = await client.post(`${BLOG}/create`, {
6770
+ title: opts.title,
6771
+ body: await readBody(opts.body, !!opts.html) ?? "",
6772
+ excerpt: opts.excerpt,
6773
+ tags: parseTags(opts.tags),
6774
+ cover: await resolveCover(client, opts.cover),
6775
+ post: resolvePost(opts.post),
6776
+ ...opts.org !== void 0 ? { organization: opts.org === "none" ? null : opts.org } : {},
6777
+ status: opts.publish ? "published" : "draft"
6778
+ });
6779
+ render(created, (b) => success(`${b.status === "published" ? "Published" : "Saved draft"} "${b.title}" (${b.id})`));
6780
+ });
6781
+ writeOptions(blog.command("update <id>")).description("Change a post; only the fields you pass are changed").action(async (id, opts) => {
6782
+ const body = Object.fromEntries(
6783
+ Object.entries({
6784
+ title: opts.title,
6785
+ body: await readBody(opts.body, !!opts.html),
6786
+ excerpt: opts.excerpt,
6787
+ tags: parseTags(opts.tags),
6788
+ post: resolvePost(opts.post),
6789
+ organization: opts.org === void 0 ? void 0 : opts.org === "none" ? null : opts.org
6790
+ }).filter(([, v]) => v !== void 0)
6791
+ );
6792
+ const client = backreelClient(globals2);
6793
+ const cover = await resolveCover(client, opts.cover);
6794
+ if (cover !== void 0) body.cover = cover;
6795
+ if (!Object.keys(body).length) throw new Error("Nothing to change \u2014 pass at least one field");
6796
+ const updated = await client.put(`${BLOG}/list/${id}`, body);
6797
+ render(updated, (b) => success(`Updated "${b.title}"`));
6798
+ });
6799
+ blog.command("publish <id>").description("Publish a draft").action(async (id) => {
6800
+ const client = backreelClient(globals2);
6801
+ const updated = await client.put(`${BLOG}/list/${id}`, { status: "published" });
6802
+ render(updated, (b) => {
6803
+ success(`Published "${b.title}"`);
6804
+ if (b.hidden) info(color.yellow(`! An admin has hidden this post: ${b.hiddenReason || "no reason given"}`));
6805
+ });
6806
+ });
6807
+ blog.command("unpublish <id>").description("Move a post back to drafts").action(async (id) => {
6808
+ const client = backreelClient(globals2);
6809
+ const updated = await client.put(`${BLOG}/list/${id}`, { status: "draft" });
6810
+ render(updated, (b) => success(`"${b.title}" is a draft again`));
6811
+ });
6812
+ blog.command("delete <id>").alias("rm").description("Delete a post").option("-y, --yes", "skip confirmation").action(async (id, opts) => {
6813
+ const client = backreelClient(globals2);
6814
+ const b = await client.getFresh(`${BLOG}/list/${id}`);
6815
+ if (!await confirm(`Delete "${b.title}"?`, opts.yes)) return info("Aborted");
6816
+ const result = await client.delete(`${BLOG}/list/${id}`);
6817
+ render(result, () => success(`Deleted "${b.title}"`));
6818
+ });
6819
+ return blog;
6820
+ }
6821
+
6442
6822
  // src/commands/backreel-enquiry.ts
6443
6823
  import { writeFile as writeFile4 } from "fs/promises";
6444
6824
  import { basename as basename7 } from "path";
6445
- import { Command as Command20 } from "commander";
6825
+ import { Command as Command21 } from "commander";
6446
6826
  var ENQ = "/backreel/enquiry";
6447
6827
  var TYPES = ["collaboration", "new_project", "investment", "licensing", "access_request", "general"];
6448
6828
  var STATUSES = ["new", "in_discussion", "accepted", "declined", "archived", "withdrawn"];
@@ -6512,7 +6892,7 @@ function printEnquiry(e, messages) {
6512
6892
  }
6513
6893
  }
6514
6894
  function enquiryCommand(globals2) {
6515
- const enq = new Command20("enquiry").aliases(["enquiries", "enq"]).description("Enquiries about your posts: read, reply, accept / decline, grant partner access");
6895
+ const enq = new Command21("enquiry").aliases(["enquiries", "enq"]).description("Enquiries about your posts: read, reply, accept / decline, grant partner access");
6516
6896
  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
6897
  const client = backreelClient(globals2);
6518
6898
  const result = await client.get(`${ENQ}/search`, {
@@ -6574,7 +6954,7 @@ function enquiryCommand(globals2) {
6574
6954
  }
6575
6955
 
6576
6956
  // src/commands/backreel-fund.ts
6577
- import { Command as Command21 } from "commander";
6957
+ import { Command as Command22 } from "commander";
6578
6958
  var CURRENCIES = ["hkd", "usd", "twd", "sgd", "eur", "gbp"];
6579
6959
  var MAX_TIERS = 10;
6580
6960
  var num = (value, flag, min = 0) => {
@@ -6662,7 +7042,7 @@ function printSupporters(post) {
6662
7042
  ]);
6663
7043
  }
6664
7044
  function fundCommand(globals2) {
6665
- const fund = new Command21("fund").aliases(["funding", "campaign"]).description("Crowdfunding campaign, donation settings and tiers, offline supporters, donation records");
7045
+ const fund = new Command22("fund").aliases(["funding", "campaign"]).description("Crowdfunding campaign, donation settings and tiers, offline supporters, donation records");
6666
7046
  fund.command("show <postId>", { isDefault: true }).description("Campaign progress, donation settings and tiers").action(async (postId) => {
6667
7047
  const client = backreelClient(globals2);
6668
7048
  const post = await loadPost(client, postId);
@@ -6808,6 +7188,640 @@ function fundCommand(globals2) {
6808
7188
  return fund;
6809
7189
  }
6810
7190
 
7191
+ // src/commands/backreel-job.ts
7192
+ import { Command as Command23 } from "commander";
7193
+ var JOBS = "/backreel/job";
7194
+ var CATEGORIES2 = ["short_video", "advertising", "movie", "short_drama", "music_video", "animation", "other"];
7195
+ var STATUSES2 = ["open", "closed", "awarded", "completed", "cancelled"];
7196
+ var BID_STATUSES = ["submitted", "withdrawn", "awarded", "declined"];
7197
+ var RESOLUTIONS = ["480p", "720p", "1080p", "2k", "4k"];
7198
+ var SORTS2 = ["newest", "deadline", "budget"];
7199
+ function parseDuration(value) {
7200
+ const t = value.trim().toLowerCase();
7201
+ if (/^\d+$/.test(t)) return Number(t);
7202
+ const colon = t.match(/^(\d+):(\d{1,2})$/);
7203
+ if (colon) return Number(colon[1]) * 60 + Number(colon[2]);
7204
+ const m = t.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
7205
+ if (!m || !t) throw new Error(`--duration "${value}" \u2014 use seconds, 3m, 1h30m or 2:30`);
7206
+ return Number(m[1] || 0) * 3600 + Number(m[2] || 0) * 60 + Number(m[3] || 0);
7207
+ }
7208
+ function parseDay(value, flag) {
7209
+ const d = /^\d{4}-\d{2}-\d{2}$/.test(value) ? /* @__PURE__ */ new Date(`${value}T23:59:00`) : new Date(value);
7210
+ if (Number.isNaN(d.getTime())) throw new Error(`${flag} "${value}" is not a date (YYYY-MM-DD)`);
7211
+ return d.toISOString();
7212
+ }
7213
+ var usd = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
7214
+ var fmt = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
7215
+ function budgetText(j) {
7216
+ if (j.budgetNegotiable && j.budgetMin == null && j.budgetMax == null) return "negotiable";
7217
+ const cur = j.currency === "usd" || !j.currency ? usd : (n) => `${String(j.currency).toUpperCase()} ${fmt(n)}`;
7218
+ 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) : "";
7219
+ return [cash, j.budgetCredits ? `${fmt(j.budgetCredits)} credits` : ""].filter(Boolean).join(" + ");
7220
+ }
7221
+ function jobOptions2(cmd) {
7222
+ 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');
7223
+ }
7224
+ async function jobBody(opts) {
7225
+ const body = {};
7226
+ if (opts.category) body.category = oneOf2(opts.category, CATEGORIES2, "--category");
7227
+ if (opts.brief !== void 0) body.brief = await readValue(opts.brief);
7228
+ if (opts.deliverables !== void 0) body.deliverables = await readValue(opts.deliverables);
7229
+ if (opts.budget !== void 0) {
7230
+ if (opts.budget === "none") Object.assign(body, { budgetMin: null, budgetMax: null });
7231
+ else {
7232
+ const [lo, hi] = String(opts.budget).split("-").map((v) => Number(v.replace(/[, ]/g, "")));
7233
+ if (!(lo >= 0) || hi !== void 0 && !(hi >= lo)) throw new Error("--budget must be like 8000 or 5000-10000");
7234
+ Object.assign(body, { budgetMin: lo, budgetMax: hi ?? lo });
7235
+ }
7236
+ body.currency = String(opts.currency || "USD").toUpperCase();
7237
+ }
7238
+ if (opts.negotiable) body.budgetNegotiable = true;
7239
+ if (opts.credits !== void 0) body.budgetCredits = Number(opts.credits) > 0 ? Math.round(Number(opts.credits)) : null;
7240
+ if (opts.duration !== void 0) body.durationSeconds = parseDuration(opts.duration);
7241
+ if (opts.resolution) body.resolution = oneOf2(opts.resolution, RESOLUTIONS, "--resolution");
7242
+ if (opts.upscale !== void 0 && process.argv.some((a) => a === "--upscale" || a === "--no-upscale")) {
7243
+ body.allowUpscale = !!opts.upscale;
7244
+ }
7245
+ if (opts.model) body.requiredModels = opts.model.slice(0, 10);
7246
+ if (opts.deadline) body.bidDeadline = parseDay(opts.deadline, "--deadline");
7247
+ if (opts.delivery !== void 0) body.deliveryDate = opts.delivery === "none" ? null : parseDay(opts.delivery, "--delivery");
7248
+ if (opts.location !== void 0) body.location = opts.location;
7249
+ if (opts.link) body.referenceLinks = opts.link.slice(0, 5);
7250
+ if (opts.verifiedOnly) body.verifiedOnly = true;
7251
+ if (opts.openToAll) body.verifiedOnly = false;
7252
+ if (opts.org !== void 0) body.organization = opts.org === "none" ? null : opts.org;
7253
+ return body;
7254
+ }
7255
+ function printJob(j) {
7256
+ const req = [
7257
+ j.durationSeconds ? `${Math.floor(j.durationSeconds / 60)}m${j.durationSeconds % 60 ? `${j.durationSeconds % 60}s` : ""}` : "",
7258
+ j.resolution ? `${j.resolution}${j.allowUpscale ? " (upscale ok)" : j.allowUpscale === false ? " (no upscale)" : ""}` : "",
7259
+ j.requiredModels?.length ? j.requiredModels.join(", ") : ""
7260
+ ].filter(Boolean);
7261
+ detail([
7262
+ ["id", j.id],
7263
+ ["title", j.title],
7264
+ ["category", j.category],
7265
+ ["status", [j.status, j.hidden ? color.yellow("hidden") : "", j.verifiedOnly ? "verified only" : ""].filter(Boolean).join(" \xB7 ")],
7266
+ ["poster", j.organization?.name ? `${j.organization.name} (org)` : j.poster?.name],
7267
+ ["budget", budgetText(j)],
7268
+ ["requirements", req.join(" \xB7 ") || color.dim("\u2014")],
7269
+ ["bid deadline", j.bidDeadline ? `${new Date(j.bidDeadline).toLocaleString()} (${relativeTime(j.bidDeadline)})` : ""],
7270
+ ["delivery", j.deliveryDate ? new Date(j.deliveryDate).toLocaleDateString() : ""],
7271
+ ["location", j.location],
7272
+ ["bids", j.bidCount],
7273
+ ["my bids", (j.myBids || []).map((b) => `${b.id} \xB7 ${b.status}${b.organization ? ` \xB7 as ${b.organization.name}` : ""}`).join("\n") || void 0],
7274
+ ["contract", j.contractId],
7275
+ ["brief", j.brief],
7276
+ ["deliverables", j.deliverables],
7277
+ ["links", (j.referenceLinks || []).join("\n")]
7278
+ ]);
7279
+ }
7280
+ function printJobs2(rows) {
7281
+ table(rows, [
7282
+ { header: "id", value: (j) => j.id },
7283
+ { header: "title", value: (j) => truncate(j.title, 30) },
7284
+ { header: "category", value: (j) => j.category },
7285
+ { header: "status", value: (j) => j.hidden ? `${j.status} ${color.yellow("hidden")}` : j.status },
7286
+ { header: "budget", value: (j) => truncate(budgetText(j), 28) },
7287
+ { header: "bids", value: (j) => j.bidCount ?? "" },
7288
+ { header: "deadline", value: (j) => relativeTime(j.bidDeadline) }
7289
+ ]);
7290
+ }
7291
+ function printBids(rows) {
7292
+ table(rows, [
7293
+ { header: "bid", value: (b) => b.id },
7294
+ { header: "job", value: (b) => truncate(b.job?.title, 24) },
7295
+ { header: "bidder", value: (b) => truncate(b.organization?.name || b.bidder?.name, 18) },
7296
+ { header: "status", value: (b) => b.status },
7297
+ { header: "price", value: (b) => b.price == null ? "" : b.currency === "usd" || !b.currency ? usd(b.price) : `${b.currency.toUpperCase()} ${fmt(b.price)}` },
7298
+ { header: "credits", value: (b) => fmt(b.creditsPrice) },
7299
+ { header: "days", value: (b) => b.timelineDays ?? "" },
7300
+ { header: "pitch", value: (b) => truncate(b.pitch, 30) }
7301
+ ]);
7302
+ }
7303
+ var pageNote = (r) => {
7304
+ const p = r?.pagination;
7305
+ if (p) info(color.dim(`${p.total} total \xB7 page ${p.page}${p.hasNext ? ` \xB7 more: --page ${p.page + 1}` : ""}`));
7306
+ };
7307
+ function bidOptions(cmd) {
7308
+ 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');
7309
+ }
7310
+ async function bidBody(opts) {
7311
+ const body = {};
7312
+ if (opts.price !== void 0) {
7313
+ body.price = opts.price === "none" ? null : Number(opts.price);
7314
+ body.currency = String(opts.currency || "USD").toUpperCase();
7315
+ }
7316
+ if (opts.credits !== void 0) body.creditsPrice = Number(opts.credits) > 0 ? Math.round(Number(opts.credits)) : null;
7317
+ if (opts.days !== void 0) body.timelineDays = Number(opts.days);
7318
+ if (opts.pitch !== void 0) body.pitch = await readValue(opts.pitch);
7319
+ if (opts.work) body.works = opts.work.slice(0, 3);
7320
+ if (opts.org !== void 0) body.organization = opts.org === "none" ? null : opts.org;
7321
+ return body;
7322
+ }
7323
+ function jobCommand(globals2) {
7324
+ const job = new Command23("job").aliases(["jobs", "plaza"]).description("Job Plaza: post jobs, place sealed bids, award a bid");
7325
+ 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) => {
7326
+ const client = backreelClient(globals2);
7327
+ const query = { page: Number(opts.page), limit: Number(opts.limit), organization: opts.org };
7328
+ if (opts.mine) {
7329
+ if (opts.status) query.status = oneOf2(opts.status, STATUSES2, "--status");
7330
+ } else {
7331
+ Object.assign(query, {
7332
+ state: opts.all ? "all" : "open",
7333
+ category: opts.category ? oneOf2(opts.category, CATEGORIES2, "--category") : void 0,
7334
+ sort: opts.sort ? oneOf2(opts.sort, SORTS2, "--sort") : void 0,
7335
+ verifiedOnly: opts.verifiedOnly ? true : void 0,
7336
+ keyword: opts.keyword
7337
+ });
7338
+ }
7339
+ const result = await client.getFresh(`${JOBS}/${opts.mine ? "search/my" : "search"}`, query);
7340
+ render(result, (r) => {
7341
+ printJobs2(r?.data || []);
7342
+ pageNote(r);
7343
+ });
7344
+ });
7345
+ job.command("get <jobId>").description("Show a job (and your bid on it, if any)").action(async (id) => {
7346
+ const client = backreelClient(globals2);
7347
+ const j = await client.getFresh(`${JOBS}/list/${id}`);
7348
+ render(j, () => printJob(j));
7349
+ });
7350
+ job.command("quota").description("How many bids you have left this month").action(async () => {
7351
+ const client = backreelClient(globals2);
7352
+ const q = await client.getFresh(`${JOBS}/quota`);
7353
+ render(q, () => {
7354
+ info(color.dim(`Month ${q.month}`));
7355
+ table(q.identities || [], [
7356
+ { header: "bid as", value: (it) => it.type === "org" ? `${it.name} (org ${it.id})` : "you" },
7357
+ { header: "used", value: (it) => `${it.used} / ${it.limit == null || it.limit === Infinity ? "\u221E" : it.limit}` },
7358
+ { header: "verified", value: (it) => it.verified ? "yes" : color.dim("no") }
7359
+ ]);
7360
+ });
7361
+ });
7362
+ jobOptions2(job.command("create <title>").description("Post a job (goes live immediately)")).action(
7363
+ async (title, opts) => {
7364
+ const client = backreelClient(globals2);
7365
+ const body = { ...await jobBody(opts), title };
7366
+ for (const f of ["category", "brief", "deliverables", "bidDeadline"]) {
7367
+ if (body[f] === void 0) throw new Error(`--${f === "bidDeadline" ? "deadline" : f} is required`);
7368
+ }
7369
+ const created = await client.post(`${JOBS}/create`, body);
7370
+ render(created, (j) => success(`Posted "${j.title}" \u2014 ${j.id}`));
7371
+ }
7372
+ );
7373
+ jobOptions2(job.command("set <jobId>").description("Edit your job; only the flags you pass change").option("--title <text>", "title")).action(
7374
+ async (id, opts) => {
7375
+ const client = backreelClient(globals2);
7376
+ const body = await jobBody(opts);
7377
+ if (opts.title) body.title = opts.title;
7378
+ if (!Object.keys(body).length) throw new Error("Nothing to change \u2014 pass at least one flag");
7379
+ const updated = await client.put(`${JOBS}/list/${id}`, body);
7380
+ render(updated, (j) => success(`Updated "${j.title}": ${Object.keys(body).join(", ")}`));
7381
+ }
7382
+ );
7383
+ const transitions = {
7384
+ close: "Stop taking bids early",
7385
+ reopen: "Take bids again (before the deadline)",
7386
+ cancel: "Cancel the job",
7387
+ complete: "Mark the job completed (jobs with a contract complete through the contract)"
7388
+ };
7389
+ for (const [action, description] of Object.entries(transitions)) {
7390
+ job.command(`${action} <jobId>`).description(description).option("-y, --yes", "skip confirmation").action(async (id, opts) => {
7391
+ const client = backreelClient(globals2);
7392
+ if (action === "cancel" && !await confirm(`Cancel job ${id}? Bidders are notified.`, opts.yes)) return info("Aborted");
7393
+ const j = await client.post(`${JOBS}/list/${id}/${action}`);
7394
+ render(j, () => success(`"${j.title}" is now ${j.status}`));
7395
+ });
7396
+ }
7397
+ 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) => {
7398
+ const client = backreelClient(globals2);
7399
+ if (id) {
7400
+ const rows = await client.getFresh(`${JOBS}/list/${id}/bids`);
7401
+ return render(rows, (r) => printBids(r || []));
7402
+ }
7403
+ const result = await client.getFresh(`${JOBS}/bids/my`, {
7404
+ page: Number(opts.page),
7405
+ limit: Number(opts.limit),
7406
+ status: opts.status ? oneOf2(opts.status, BID_STATUSES, "--status") : void 0
7407
+ });
7408
+ render(result, (r) => {
7409
+ printBids(r?.data || []);
7410
+ pageNote(r);
7411
+ });
7412
+ });
7413
+ bidOptions(job.command("bid <jobId>").description("Place a sealed bid (uses one of your monthly bids)")).action(
7414
+ async (id, opts) => {
7415
+ const client = backreelClient(globals2);
7416
+ const body = await bidBody(opts);
7417
+ if (body.timelineDays === void 0) throw new Error("--days is required");
7418
+ if (!body.pitch) throw new Error("--pitch is required");
7419
+ const j = await client.post(`${JOBS}/list/${id}/bids`, body);
7420
+ render(j, (x) => {
7421
+ const mine = (x.myBids || []).filter((b) => b.status === "submitted");
7422
+ success(`Bid placed on "${x.title}"${mine.length ? ` \u2014 bid ${mine[mine.length - 1].id}` : ""}`);
7423
+ });
7424
+ }
7425
+ );
7426
+ bidOptions(job.command("bid-set <bidId>").description("Change your bid while bidding is open")).action(
7427
+ async (bidId, opts) => {
7428
+ const client = backreelClient(globals2);
7429
+ const body = await bidBody(opts);
7430
+ if (!Object.keys(body).length) throw new Error("Nothing to change \u2014 pass at least one flag");
7431
+ const j = await client.put(`${JOBS}/bid/${bidId}`, body);
7432
+ render(j, () => success(`Updated bid ${bidId}: ${Object.keys(body).join(", ")}`));
7433
+ }
7434
+ );
7435
+ job.command("withdraw <bidId>").description("Withdraw your bid").action(async (bidId) => {
7436
+ const client = backreelClient(globals2);
7437
+ const j = await client.post(`${JOBS}/bid/${bidId}/withdraw`);
7438
+ render(j, () => success(`Withdrew bid ${bidId}`));
7439
+ });
7440
+ 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) => {
7441
+ const client = backreelClient(globals2);
7442
+ if (!await confirm(`Award bid ${bidId}? All other bids are declined.`, opts.yes)) return info("Aborted");
7443
+ const r = await client.post(`${JOBS}/bid/${bidId}/award`);
7444
+ render(
7445
+ r,
7446
+ (x) => success(`Awarded.${x?.contractId ? ` Contract ${x.contractId} \u2014 next: wji br contract plan ${x.contractId}` : ""}`)
7447
+ );
7448
+ });
7449
+ return job;
7450
+ }
7451
+
7452
+ // src/commands/backreel-contract.ts
7453
+ import { Command as Command24 } from "commander";
7454
+ var CONTRACTS = "/backreel/contract";
7455
+ var RELEASES = ["upfront", "milestones"];
7456
+ var CR_REASONS = ["scope", "revision", "other"];
7457
+ var KIND_NAMES = {
7458
+ preprod: "Pre-production",
7459
+ lookdev: "Look development",
7460
+ rough: "Rough cut",
7461
+ final: "Final delivery",
7462
+ concept: "Concept",
7463
+ draft: "Draft"
7464
+ };
7465
+ var usd2 = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
7466
+ var fmt2 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
7467
+ var msName = (m, i) => m.title || KIND_NAMES[m.kind] || `Payment ${i + 1}`;
7468
+ async function loadContract(client, ref) {
7469
+ try {
7470
+ return await client.getFresh(`${CONTRACTS}/${ref}`);
7471
+ } catch (err) {
7472
+ if (!/404|not found|不存在/i.test(String(err?.message))) throw err;
7473
+ return client.getFresh(`${CONTRACTS}/job/${ref}`);
7474
+ }
7475
+ }
7476
+ function milestoneKey(c, n) {
7477
+ const i = Number(n);
7478
+ if (!Number.isInteger(i) || i < 1 || i > c.milestones.length) {
7479
+ throw new Error(`Payment ${n} does not exist \u2014 this contract has ${c.milestones.length}`);
7480
+ }
7481
+ return c.milestones[i - 1].key;
7482
+ }
7483
+ function nextStep(c) {
7484
+ const side = c.mySide;
7485
+ if (c.status === "planning") {
7486
+ if (!c.proposedBy) return `Draft the payment plan: wji br contract plan ${c.id} --payment "\u2026" --payment "\u2026"`;
7487
+ return c.proposedBy === side ? "Waiting for the other side to accept or edit the plan" : `Review the plan: wji br contract accept ${c.id}`;
7488
+ }
7489
+ if (c.status === "funding") return side === "poster" ? `Add the credits: wji br contract fund ${c.id}` : "Waiting for the poster to add the credits";
7490
+ if (c.status === "disputed") return `In dispute \u2014 an admin decides; add your side: wji br contract statement ${c.id} --text "\u2026"`;
7491
+ if (c.status === "in_production") {
7492
+ const i = c.milestones.findIndex((m2) => m2.status !== "approved" && m2.status !== "pending");
7493
+ if (i < 0) return "";
7494
+ const m = c.milestones[i];
7495
+ if (m.status === "submitted") return side === "poster" ? `Review payment ${i + 1}: approve or revise` : `Waiting for review of payment ${i + 1}`;
7496
+ return side === "creator" ? `Work on payment ${i + 1}, then: wji br contract submit ${c.id} ${i + 1}` : `Creator is working on payment ${i + 1}`;
7497
+ }
7498
+ return "";
7499
+ }
7500
+ function printContract(c) {
7501
+ const e = c.escrow;
7502
+ detail([
7503
+ ["id", c.id],
7504
+ ["job", `${c.job?.title} (${c.job?.id})`],
7505
+ ["status", c.status],
7506
+ ["you are", c.mySide],
7507
+ ["poster", c.posterOrg?.name ? `${c.posterOrg.name} (org)` : c.poster?.name],
7508
+ ["creator", c.creatorOrg?.name ? `${c.creatorOrg.name} (org)` : c.creator?.name],
7509
+ ["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)}%)`],
7510
+ ["credits release", c.creditsRelease === "milestones" ? "with each payment" : "all at once"],
7511
+ ["plan", c.status === "planning" ? `v${c.planVersion}${c.proposedBy ? ` proposed by ${c.proposedBy}` : ""}` : void 0],
7512
+ [
7513
+ "project credits",
7514
+ 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
7515
+ ],
7516
+ ["workspace", e?.workspaceId],
7517
+ ["cancel request", c.cancelRequest ? `${c.cancelRequest.side}${c.cancelRequest.reason ? `: ${c.cancelRequest.reason}` : ""}` : void 0],
7518
+ ["creator silent since", c.creatorSilentSince ? `${new Date(c.creatorSilentSince).toLocaleDateString()} \u2014 you can cancel alone` : void 0],
7519
+ ["next", nextStep(c) || void 0]
7520
+ ]);
7521
+ info("");
7522
+ table(c.milestones || [], [
7523
+ { header: "#", value: (_m) => String(c.milestones.indexOf(_m) + 1) },
7524
+ { header: "payment", value: (m) => truncate(msName(m, c.milestones.indexOf(m)), 22) },
7525
+ { header: "due", value: (m) => new Date(m.dueDate).toLocaleDateString() },
7526
+ { header: "cash", value: (m) => usd2(m.cashUsd) },
7527
+ ...c.creditsRelease === "milestones" ? [{ header: "credits", value: (m) => fmt2(m.credits) }] : [],
7528
+ { header: "status", value: (m) => m.status },
7529
+ { header: "revisions", value: (m) => m.revisionsUsed ? `${m.revisionsUsed}/${c.revisionAllowance}` : "" },
7530
+ {
7531
+ header: "cash paid",
7532
+ value: (m) => m.cashReceivedAt ? "received" : m.cashPaidAt ? "paid" : m.status === "approved" && m.cashUsd > 0 ? color.yellow("due") : ""
7533
+ }
7534
+ ]);
7535
+ const pending = (c.changeRequests || []).find((r) => r.status === "pending");
7536
+ if (pending) {
7537
+ info("");
7538
+ info(`${color.yellow("Change request waiting")} ${pending.id}: +${fmt2(pending.credits)} credits${pending.cashUsd ? ` + ${usd2(pending.cashUsd)}` : ""} (${pending.reason}) \u2014 ${truncate(pending.note, 60)}`);
7539
+ }
7540
+ const d = c.dispute;
7541
+ if (d && d.status !== "withdrawn") {
7542
+ info("");
7543
+ info(`${d.status === "open" ? color.yellow("Dispute open") : "Dispute decided"} \u2014 raised by ${d.side}: ${truncate(d.reason, 80)}`);
7544
+ for (const st of d.statements || []) info(color.dim(` ${st.side} \xB7 ${relativeTime(st.at)}: ${truncate(st.text, 80)}`));
7545
+ 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)}`);
7546
+ }
7547
+ }
7548
+ function parsePayment(spec, perMilestone) {
7549
+ const [title, due, cash, credits, ...rest] = spec.split("|").map((s) => s.trim());
7550
+ if (!due || cash === void 0) throw new Error(`--payment "${spec}" must be "Title|YYYY-MM-DD|cash[|credits[|deliverable]]"`);
7551
+ const n = Number(cash.replace(/[, ]/g, ""));
7552
+ if (!(n >= 0)) throw new Error(`--payment "${spec}": cash must be a number`);
7553
+ return {
7554
+ title,
7555
+ dueDate: parseDay(due, "--payment"),
7556
+ cash: n,
7557
+ deliverable: rest.join("|"),
7558
+ ...perMilestone ? { credits: Math.round(Number(credits) || 0) } : {}
7559
+ };
7560
+ }
7561
+ function contractCommand(globals2) {
7562
+ const ct = new Command24("contract").aliases(["contracts", "deal"]).description("Job contracts: payment plan, project credits, milestones, disputes");
7563
+ const run = async (ref, path, body, done) => {
7564
+ const client = backreelClient(globals2);
7565
+ const c = await loadContract(client, ref);
7566
+ const updated = await client.post(`${CONTRACTS}/${c.id}${path}`, body);
7567
+ render(updated, (u) => {
7568
+ if (done) success(done(u));
7569
+ const step = nextStep(u);
7570
+ if (step) info(color.dim(`Next: ${step}`));
7571
+ });
7572
+ return updated;
7573
+ };
7574
+ 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) => {
7575
+ const client = backreelClient(globals2);
7576
+ const result = await client.getFresh(`${CONTRACTS}/my`, {
7577
+ page: Number(opts.page),
7578
+ status: opts.status || (opts.done ? "done" : "active")
7579
+ });
7580
+ render(result, (r) => {
7581
+ table(r?.data || [], [
7582
+ { header: "id", value: (c) => c.id },
7583
+ { header: "job", value: (c) => truncate(c.job?.title, 28) },
7584
+ { header: "you", value: (c) => c.mySide },
7585
+ { header: "status", value: (c) => c.status },
7586
+ { header: "total", value: (c) => usd2(c.totalUsd) },
7587
+ { header: "progress", value: (c) => `${c.approvedCount}/${c.milestoneCount}` },
7588
+ { header: "updated", value: (c) => relativeTime(c.updatedAt) }
7589
+ ]);
7590
+ const p = r?.pagination;
7591
+ if (p) info(color.dim(`${p.total} total \xB7 page ${p.page}${p.hasNext ? ` \xB7 more: --page ${p.page + 1}` : ""}`));
7592
+ });
7593
+ });
7594
+ 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) => {
7595
+ const client = backreelClient(globals2);
7596
+ const c = await loadContract(client, ref);
7597
+ render(c, () => {
7598
+ printContract(c);
7599
+ if (opts.events) {
7600
+ info("");
7601
+ for (const e of c.events || []) info(color.dim(`${new Date(e.at).toLocaleString()} \xB7 ${e.side || "system"} \xB7 ${e.type}`));
7602
+ }
7603
+ });
7604
+ });
7605
+ 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) => {
7606
+ const client = backreelClient(globals2);
7607
+ const c = await loadContract(client, ref);
7608
+ const release = opts.release ? oneOf2(opts.release, RELEASES, "--release") : c.creditsRelease ?? "upfront";
7609
+ const perMilestone = release === "milestones";
7610
+ let milestones;
7611
+ let currency = String(opts.currency || "USD").toUpperCase();
7612
+ if (opts.payment) {
7613
+ milestones = opts.payment.map((spec) => parsePayment(spec, perMilestone));
7614
+ if (milestones.length < 2 || milestones.length > 3) throw new Error("A payment plan has 2 or 3 payments");
7615
+ } else {
7616
+ currency = "USD";
7617
+ milestones = c.milestones.map((m) => ({
7618
+ key: m.key,
7619
+ kind: m.kind,
7620
+ title: m.title,
7621
+ deliverable: m.deliverable,
7622
+ dueDate: m.dueDate,
7623
+ cash: m.cashUsd,
7624
+ ...perMilestone ? { credits: m.credits } : {}
7625
+ }));
7626
+ }
7627
+ const body = { currency, creditsRelease: release, milestones };
7628
+ if (!perMilestone) body.credits = opts.credits !== void 0 ? Math.round(Number(opts.credits)) : c.credits;
7629
+ const updated = await client.put(`${CONTRACTS}/${c.id}/plan`, body);
7630
+ render(updated, (u) => {
7631
+ success(`Sent plan v${u.planVersion}: ${usd2(u.totalUsd)} (${Math.round(u.creditsShare * 100)}% credits)`);
7632
+ info(color.dim(`Next: ${nextStep(u)}`));
7633
+ });
7634
+ });
7635
+ ct.command("accept <contract>").description("Accept the other side's plan (locks the contract)").action(async (ref) => {
7636
+ const client = backreelClient(globals2);
7637
+ const c = await loadContract(client, ref);
7638
+ const updated = await client.post(`${CONTRACTS}/${c.id}/accept`, { version: c.planVersion });
7639
+ render(updated, (u) => {
7640
+ success(`Accepted plan v${c.planVersion} \u2014 contract is ${u.status}`);
7641
+ info(color.dim(`Next: ${nextStep(u)}`));
7642
+ });
7643
+ });
7644
+ 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) => {
7645
+ const client = backreelClient(globals2);
7646
+ const c = await loadContract(client, ref);
7647
+ if (c.myCredits != null && c.myCredits < c.credits) {
7648
+ throw new Error(`You have ${fmt2(c.myCredits)} general credits; this contract needs ${fmt2(c.credits)}. Top up on WJI first.`);
7649
+ }
7650
+ if (!await confirm(`Add ${fmt2(c.credits)} credits to "${c.job?.title}" and start production?`, opts.yes)) return info("Aborted");
7651
+ await run(c.id, "/fund", void 0, (u) => `Funded \u2014 project workspace ${u.escrow?.workspaceId}`);
7652
+ });
7653
+ 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) => {
7654
+ const client = backreelClient(globals2);
7655
+ const c = await loadContract(client, ref);
7656
+ const note = opts.note ? await readValue(opts.note) : "";
7657
+ if (!note && !opts.link) throw new Error("Pass --note and/or --link");
7658
+ await run(c.id, `/milestones/${milestoneKey(c, n)}/submit`, { note, links: opts.link || [] }, () => `Submitted payment ${n}`);
7659
+ });
7660
+ for (const action of ["approve", "revise"]) {
7661
+ 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) => {
7662
+ const client = backreelClient(globals2);
7663
+ const c = await loadContract(client, ref);
7664
+ const note = opts.note ? await readValue(opts.note) : "";
7665
+ if (action === "revise" && !note) throw new Error("--note is required to ask for a revision");
7666
+ await run(
7667
+ c.id,
7668
+ `/milestones/${milestoneKey(c, n)}/${action}`,
7669
+ { note },
7670
+ (u) => action === "approve" ? u.status === "completed" ? "Approved \u2014 contract completed" : `Approved payment ${n}` : `Asked for a revision on payment ${n}`
7671
+ );
7672
+ });
7673
+ }
7674
+ ct.command("paid <contract> <payment>").description("Poster: mark the cash for an approved payment as paid").action(async (ref, n) => {
7675
+ const client = backreelClient(globals2);
7676
+ const c = await loadContract(client, ref);
7677
+ await run(c.id, `/milestones/${milestoneKey(c, n)}/cash-paid`, void 0, () => `Marked payment ${n} as paid`);
7678
+ });
7679
+ ct.command("received <contract> <payment>").description("Creator: confirm the cash for a payment arrived").action(async (ref, n) => {
7680
+ const client = backreelClient(globals2);
7681
+ const c = await loadContract(client, ref);
7682
+ await run(c.id, `/milestones/${milestoneKey(c, n)}/cash-received`, void 0, () => `Confirmed payment ${n} received`);
7683
+ });
7684
+ 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) => {
7685
+ if (opts.withdraw) return void await run(ref, "/cancel/withdraw", void 0, () => "Cancel request withdrawn");
7686
+ if (opts.decline) return void await run(ref, "/cancel/decline", void 0, () => "Declined \u2014 the contract continues");
7687
+ if (!await confirm(`Cancel contract ${ref}?`, opts.yes)) return info("Aborted");
7688
+ await run(
7689
+ ref,
7690
+ "/cancel",
7691
+ { reason: opts.reason || "" },
7692
+ (u) => u.status === "cancelled" ? "Contract cancelled" : "Cancel request sent \u2014 the other side has to confirm"
7693
+ );
7694
+ });
7695
+ ct.command("topup <contract> <credits>").description("Creator: add your own credits to the project (unused ones come back to you)").action(async (ref, credits) => {
7696
+ const n = Math.floor(Number(credits));
7697
+ if (!(n > 0)) throw new Error("credits must be a positive number");
7698
+ await run(ref, "/topup", { credits: n }, (u) => `Topped up ${fmt2(n)} \u2014 ${fmt2(Math.max(0, u.escrow?.left ?? 0))} project credits left`);
7699
+ });
7700
+ const change = ct.command("change").description("Change requests: ask for more credits / cash when scope changes or extra revisions");
7701
+ 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) => {
7702
+ const client = backreelClient(globals2);
7703
+ const c = await loadContract(client, ref);
7704
+ const body = {
7705
+ reason: oneOf2(opts.reason, CR_REASONS, "--reason"),
7706
+ note: await readValue(opts.note),
7707
+ credits: Math.round(Number(opts.credits) || 0),
7708
+ cashUsd: opts.cash ? Number(opts.cash) : 0
7709
+ };
7710
+ if (opts.payment) body.milestoneKey = milestoneKey(c, opts.payment);
7711
+ const cashUsd = Number(body.cashUsd);
7712
+ const min = cashUsd > 0 ? Math.ceil(cashUsd * c.creditsPerUsd * 0.25 / 0.75) : 0;
7713
+ if (Number(body.credits) < min) throw new Error(`At least 25% of the extra must be credits: --credits ${min} or more`);
7714
+ await run(c.id, "/change-requests", body, () => "Change request sent to the poster");
7715
+ });
7716
+ for (const action of ["accept", "decline", "withdraw"]) {
7717
+ change.command(`${action} <contract> [requestId]`).description(
7718
+ action === "accept" ? "Poster: accept and add the extra credits from your WJI balance" : action === "decline" ? "Poster: decline" : "Creator: withdraw your request"
7719
+ ).option("--note <text>", "reply to the other side").action(async (ref, requestId, opts) => {
7720
+ const client = backreelClient(globals2);
7721
+ const c = await loadContract(client, ref);
7722
+ const id = requestId || (c.changeRequests || []).find((r) => r.status === "pending")?.id;
7723
+ if (!id) throw new Error("No pending change request");
7724
+ await run(
7725
+ c.id,
7726
+ `/change-requests/${id}/${action}`,
7727
+ action === "withdraw" ? void 0 : { note: opts.note || "" },
7728
+ () => action === "accept" ? "Accepted \u2014 contract total updated" : action === "decline" ? "Declined" : "Withdrawn"
7729
+ );
7730
+ });
7731
+ }
7732
+ 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) => {
7733
+ if (opts.withdraw) return void await run(ref, "/dispute/withdraw", void 0, () => "Dispute withdrawn \u2014 back in production");
7734
+ if (!opts.reason) throw new Error("--reason is required");
7735
+ const reason = await readValue(opts.reason);
7736
+ if (!await confirm("Open a dispute? Generation in the project workspace pauses until an admin decides.", opts.yes)) return info("Aborted");
7737
+ await run(ref, "/dispute", { reason }, () => "Dispute opened \u2014 add your side with: wji br contract statement");
7738
+ });
7739
+ 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) => {
7740
+ await run(ref, "/dispute/statement", { text: await readValue(opts.text), links: opts.link || [] }, () => "Statement added");
7741
+ });
7742
+ return ct;
7743
+ }
7744
+
7745
+ // src/commands/backreel-profile.ts
7746
+ import { Command as Command25 } from "commander";
7747
+ function orgCommand2(globals2) {
7748
+ const org = new Command25("org").aliases(["orgs"]).description("Your organizations on Backreel (post, bid and publish as one with --org)");
7749
+ org.command("list", { isDefault: true }).description("Organizations you belong to, with your role").action(async () => {
7750
+ const client = backreelClient(globals2);
7751
+ const rows = await client.getFresh("/backreel/org/mine");
7752
+ render(
7753
+ rows,
7754
+ (r) => table(r || [], [
7755
+ { header: "id", value: (o) => o.id },
7756
+ { header: "name", value: (o) => o.name },
7757
+ { header: "role", value: (o) => o.role }
7758
+ ])
7759
+ );
7760
+ });
7761
+ org.command("get <orgId>").description("An organization's public page: about, region, members, verifications").action(async (id) => {
7762
+ const client = backreelClient(globals2);
7763
+ const o = await client.getFresh(`/backreel/org/${id}`);
7764
+ render(
7765
+ o,
7766
+ () => detail([
7767
+ ["id", o.id],
7768
+ ["name", o.name],
7769
+ ["region", o.region || color.dim("\u2014")],
7770
+ ["members", o.memberCount],
7771
+ ["you", o.member ? o.member.role ?? "member" : color.dim("not a member")],
7772
+ ["verified as", (o.verifications || []).map((v) => v.kind ?? v).join(", ") || color.dim("\u2014")],
7773
+ ["about", o.description]
7774
+ ])
7775
+ );
7776
+ });
7777
+ return org;
7778
+ }
7779
+ function aboutCommand(globals2) {
7780
+ 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) => {
7781
+ const body = {};
7782
+ if (opts.text !== void 0) body.description = opts.text === "" ? "" : await readValue(opts.text);
7783
+ if (opts.region !== void 0) body.region = String(opts.region).trim().toUpperCase();
7784
+ if (!Object.keys(body).length) throw new Error("Pass --text and/or --region");
7785
+ const client = backreelClient(globals2);
7786
+ const saved = await client.put(opts.org ? `/backreel/about/org/${opts.org}` : "/backreel/about/me", body);
7787
+ render(saved, () => success(`Updated ${opts.org ? "organization" : "profile"}: ${Object.keys(body).join(", ")}`));
7788
+ });
7789
+ }
7790
+ function historyCommand2(globals2) {
7791
+ const history = new Command25("history").description("Your viewing history (works and blog posts)");
7792
+ 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) => {
7793
+ const client = backreelClient(globals2);
7794
+ const result = await client.getFresh("/backreel/view/history", {
7795
+ page: Number(opts.page),
7796
+ limit: Number(opts.limit),
7797
+ type: opts.type
7798
+ });
7799
+ render(result, (r) => {
7800
+ table(r?.data || [], [
7801
+ { header: "id", value: (h) => h.id },
7802
+ { header: "type", value: (h) => h.targetType },
7803
+ { header: "title", value: (h) => h.item ? truncate(h.item.title, 36) : color.dim("(no longer available)") },
7804
+ { header: "views", value: (h) => h.count },
7805
+ { header: "last", value: (h) => relativeTime(h.lastViewedAt) }
7806
+ ]);
7807
+ const p = r?.pagination;
7808
+ if (p) info(color.dim(`${p.total} total \xB7 page ${p.page}${p.hasNext ? ` \xB7 more: --page ${p.page + 1}` : ""}`));
7809
+ });
7810
+ });
7811
+ history.command("rm <id>").description("Remove one entry").action(async (id) => {
7812
+ const client = backreelClient(globals2);
7813
+ const r = await client.delete(`/backreel/view/history/${id}`);
7814
+ render(r, (x) => success(x?.removed ? "Removed" : "Nothing to remove"));
7815
+ });
7816
+ history.command("clear").description("Clear your history").option("--type <t>", "only post or blog entries").option("-y, --yes", "skip confirmation").action(async (opts) => {
7817
+ if (!await confirm("Clear your Backreel viewing history?", opts.yes)) return info("Aborted");
7818
+ const client = backreelClient(globals2);
7819
+ const r = await client.delete("/backreel/view/history", opts.type ? { type: opts.type } : void 0);
7820
+ render(r, (x) => success(`Removed ${x?.removed ?? 0} entries`));
7821
+ });
7822
+ return history;
7823
+ }
7824
+
6811
7825
  // src/commands/backreel.ts
6812
7826
  var KINDS = ["showcase", "crowdfunding"];
6813
7827
  var PROJECT_STATUSES = ["planning", "pre_production", "in_production", "post_production", "completed"];
@@ -6817,11 +7831,12 @@ var LOGLINE_MAX = 200;
6817
7831
  var VERIFICATION_KINDS = ["investor", "production_company", "brand", "distributor", "sales_agent"];
6818
7832
  var CREDIT_FIELDS = ["kind", "name", "role", "user", "photo", "avatarUrl", "link", "source", "npcId", "order"];
6819
7833
  function backreelCommand(globals2) {
6820
- const br = new Command22("backreel").aliases(["br"]).description("Manage your Backreel posts (works) and their credits, including NPC cast");
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) => {
7834
+ const br = new Command26("backreel").aliases(["br"]).description("Manage your Backreel posts (works) and their credits, including NPC cast");
7835
+ 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
7836
  const client = backreelClient(globals2);
6823
7837
  const result = await client.get(`${POSTS}/${opts.all ? "search" : "search/my"}`, {
6824
7838
  keyword: opts.keyword,
7839
+ organization: opts.org,
6825
7840
  page: Number(opts.page),
6826
7841
  limit: Number(opts.limit)
6827
7842
  });
@@ -6990,7 +8005,13 @@ function backreelCommand(globals2) {
6990
8005
  });
6991
8006
  br.addCommand(episodeCommand(globals2));
6992
8007
  br.addCommand(enquiryCommand(globals2));
8008
+ br.addCommand(blogCommand(globals2));
6993
8009
  br.addCommand(fundCommand(globals2));
8010
+ br.addCommand(jobCommand(globals2));
8011
+ br.addCommand(contractCommand(globals2));
8012
+ br.addCommand(orgCommand2(globals2));
8013
+ br.addCommand(aboutCommand(globals2));
8014
+ br.addCommand(historyCommand2(globals2));
6994
8015
  const credit = br.command("credit").aliases(["credits"]).description("Cast and crew of a post (max 30)");
6995
8016
  credit.command("list <postId>", { isDefault: true }).description("Numbered credits").action(async (id) => {
6996
8017
  const client = backreelClient(globals2);
@@ -7049,7 +8070,7 @@ function backreelCommand(globals2) {
7049
8070
  return br;
7050
8071
  }
7051
8072
  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");
8073
+ 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
8074
  }
7054
8075
  async function postBody(opts) {
7055
8076
  const body = { ...await readJson(opts.body) || {} };
@@ -7060,6 +8081,7 @@ async function postBody(opts) {
7060
8081
  body.logline = logline;
7061
8082
  }
7062
8083
  if (opts.category) body.category = opts.category;
8084
+ if (opts.org !== void 0) body.organization = opts.org === "none" ? null : opts.org;
7063
8085
  if (opts.projectStatus) body.projectStatus = oneOf2(opts.projectStatus, PROJECT_STATUSES, "--project-status");
7064
8086
  if (opts.tags !== void 0) body.tags = String(opts.tags).split(",").map((t) => t.trim()).filter(Boolean);
7065
8087
  if (opts.description !== void 0) body.description = toHtml(await readValue(opts.description));
@@ -7130,7 +8152,7 @@ process.stdout.on("error", (err) => {
7130
8152
  if (err.code === "EPIPE") process.exit(0);
7131
8153
  throw err;
7132
8154
  });
7133
- var program = new Command23();
8155
+ var program = new Command27();
7134
8156
  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
8157
  "--detail <level>",
7136
8158
  "how much to return: brief | standard | full (default standard for --json; env WJI_DETAIL)"