@wji.ai/cli 0.9.4 → 0.9.5

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
@@ -6106,6 +6106,7 @@ function printChanges2(changes, warnings = []) {
6106
6106
  }
6107
6107
 
6108
6108
  // src/commands/backreel.ts
6109
+ import { stat as stat6 } from "fs/promises";
6109
6110
  import { Command as Command26 } from "commander";
6110
6111
 
6111
6112
  // src/lib/backreel.ts
@@ -6382,11 +6383,18 @@ async function resolveSource(client, opts, postId, file) {
6382
6383
  const asset = await client.upload("/assets/v2/upload", file, {});
6383
6384
  return { video: asset.id };
6384
6385
  }
6386
+ return { vodVid: await uploadVideoToVod(client, file, { postId, title: opts.title }) };
6387
+ }
6388
+ async function uploadVideoToVod(client, file, opts = {}) {
6389
+ const ext = extname2(file).toLowerCase();
6390
+ if (!VIDEO_EXTS.includes(ext)) throw new Error(`${basename6(file)}: videos must be ${VIDEO_EXTS.join(" / ")}`);
6391
+ const size = (await stat5(file)).size;
6392
+ if (size > VIDEO_MAX_BYTES) throw new Error(`${basename6(file)} is over 500 MB`);
6385
6393
  const token = await client.post("/backreel/vod/upload-token");
6386
6394
  const vid = await uploadToVod(token, file, (step) => info(color.dim(`VOD: ${step}\u2026`)));
6387
- const claimed = await client.post("/backreel/vod/claim", { vid, postId, title: opts.title?.slice(0, 128) });
6395
+ const claimed = await client.post("/backreel/vod/claim", { vid, postId: opts.postId ?? void 0, title: opts.title?.slice(0, 128) });
6388
6396
  info(color.dim(`VOD ${vid}: ${claimed?.status ?? "claimed"}`));
6389
- return { vodVid: vid };
6397
+ return vid;
6390
6398
  }
6391
6399
  function printEpisodes(post) {
6392
6400
  const rows = [...post.episodes || []].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
@@ -7937,7 +7945,7 @@ function backreelCommand(globals2) {
7937
7945
  br.command("create <title>").description("Create a post (always a draft until `backreel publish`)")
7938
7946
  ).action(async (title, opts) => {
7939
7947
  const client = backreelClient(globals2);
7940
- const body = { kind: "showcase", description: "", ...await postBody(opts), title };
7948
+ const body = { kind: "showcase", description: "", ...await postBody(opts, { client, title }), title };
7941
7949
  const created = await client.post(`${POSTS}/create`, body);
7942
7950
  render(created, (p) => success(`Created draft "${p.title}" \u2014 ${p.id}`));
7943
7951
  });
@@ -7945,7 +7953,7 @@ function backreelCommand(globals2) {
7945
7953
  br.command("set <postId>").description("Update a post; only the fields you pass change").option("--title <text>", "title")
7946
7954
  ).action(async (id, opts) => {
7947
7955
  const client = backreelClient(globals2);
7948
- const body = await postBody(opts);
7956
+ const body = await postBody(opts, { client, postId: id, title: opts.title });
7949
7957
  if (opts.title) body.title = opts.title;
7950
7958
  if (!Object.keys(body).length) throw new Error("Nothing to change \u2014 pass at least one field flag");
7951
7959
  const updated = await client.put(`${POSTS}/list/${id}`, body);
@@ -8143,9 +8151,20 @@ function backreelCommand(globals2) {
8143
8151
  return br;
8144
8152
  }
8145
8153
  function postOptions(cmd) {
8146
- 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 (\u2264500 MB, any length)").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("--on-slate <on|off>", "crowdfunding only: also list it on the Slate, not just the Crowdfunding page (default off)").option("--body <json>", "any other post fields as JSON (@file / - supported); flags win");
8154
+ 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 <file|vid|assetId|none>", "trailer: a local video file is uploaded to VOD (\u2264500 MB, any length); vod:<Vid> reuses an upload; a WJI asset id is the old way; none clears").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("--on-slate <on|off>", "crowdfunding only: also list it on the Slate, not just the Crowdfunding page (default off)").option("--body <json>", "any other post fields as JSON (@file / - supported); flags win");
8155
+ }
8156
+ async function trailerFields(value, ctx) {
8157
+ if (value === "none") return { trailer: null, trailerVod: null };
8158
+ if (value.startsWith("vod:")) return { trailerVod: { vid: value.slice(4) } };
8159
+ const isFile = await stat6(value).then((s) => s.isFile()).catch(() => false);
8160
+ if (isFile) {
8161
+ if (!ctx.client) throw new Error("--trailer <file> needs a signed-in client");
8162
+ const vid = await uploadVideoToVod(ctx.client, value, { postId: ctx.postId, title: `${ctx.title || "trailer"} \xB7 trailer` });
8163
+ return { trailerVod: { vid } };
8164
+ }
8165
+ return { trailer: value };
8147
8166
  }
8148
- async function postBody(opts) {
8167
+ async function postBody(opts, ctx = {}) {
8149
8168
  const body = { ...await readJson(opts.body) || {} };
8150
8169
  if (opts.kind) body.kind = oneOf2(opts.kind, KINDS, "--kind");
8151
8170
  if (opts.logline !== void 0) {
@@ -8161,7 +8180,7 @@ async function postBody(opts) {
8161
8180
  if (opts.description !== void 0) body.description = toHtml(await readValue(opts.description));
8162
8181
  if (opts.cover) body.cover = opts.cover;
8163
8182
  if (opts.coverPortrait) body.coverPortrait = opts.coverPortrait;
8164
- if (opts.trailer) body.trailer = opts.trailer;
8183
+ if (opts.trailer) Object.assign(body, await trailerFields(String(opts.trailer), ctx));
8165
8184
  if (opts.media) {
8166
8185
  const media = opts.media.filter((m) => m !== "none");
8167
8186
  if (media.length > MEDIA_MAX) throw new Error(`--media takes at most ${MEDIA_MAX} items (the web's LookBook limit)`);
@@ -8201,7 +8220,7 @@ function printPost(p) {
8201
8220
  ["deadline", p.campaign?.deadline ? String(p.campaign.deadline).slice(0, 10) : void 0],
8202
8221
  ["on slate", p.kind === "crowdfunding" ? p.showOnSlate ? "yes" : "no (Crowdfunding page only)" : void 0],
8203
8222
  ["donations", p.donation?.enabled ? "on" : void 0],
8204
- ["trailer", p.trailer ? `${assetLabel(p.trailer)}${p.trailerDurationSec ? ` (${p.trailerDurationSec}s)` : ""}` : void 0],
8223
+ ["trailer", p.trailerVod?.vid ? `vod ${p.trailerVod.vid} \xB7 ${p.trailerVod.status}${p.trailerDurationSec ? ` (${Math.round(p.trailerDurationSec)}s)` : ""}` : p.trailer ? `${assetLabel(p.trailer)}${p.trailerDurationSec ? ` (${p.trailerDurationSec}s)` : ""}` : void 0],
8205
8224
  ["creator", p.creator?.name],
8206
8225
  ["likes", p.likeCount],
8207
8226
  ["interested", p.interestCount ? `${p.interestCount} (members get new-episode and campaign-result notices)` : void 0],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wji.ai/cli",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Command line interface for the WJI AI platform",
5
5
  "license": "UNLICENSED",
6
6
  "author": "WJI AI",
@@ -26,7 +26,7 @@ wji br list # your posts, drafts included
26
26
  | kind | `showcase` (episodes, trailer) or `crowdfunding` (goal, supporters) — switching wipes the other side's data | `br create --kind`, `br set --kind` |
27
27
  | title / logline / description | logline = one public line ≤200 chars; description = HTML or plain text | `br set --logline`, `-d @file.md` |
28
28
  | images | 16:9 cover, 9:16 portrait, LookBook gallery (≤9), credit photos | `br image <file> --slot … --post` |
29
- | trailer | a video asset, ≤500 MB, any length | `wji asset upload` → `br set --trailer` |
29
+ | trailer | a video on VOD, ≤500 MB, any length | `br set --trailer <file>` |
30
30
  | episodes | ≤30, each with its own viewer tier, hosted on VOD | `br episode …` |
31
31
  | credits | cast / crew / creative, ≤30, may link a WJI user or an NPC | `br credit …` |
32
32
  | campaign + support | funding type (credits / cash / both), credits goal, cash goal, deadline, tiers, offline supporters | `br fund …` |
@@ -8,18 +8,22 @@
8
8
  | count | one | ≤30 |
9
9
  | length | no limit | no limit |
10
10
  | size | ≤500 MB | ≤500 MB |
11
- | storage | WJI asset (public URL) | BytePlus VOD (signed URL per play, ~5 min) |
11
+ | storage | BytePlus VOD (signed URL per play, ~5 min); older trailers may be a WJI asset | BytePlus VOD (signed URL per play, ~5 min) |
12
12
  | gated | no — always public | per episode: public / member / supporter / partner |
13
13
 
14
14
  ### Trailer
15
15
 
16
16
  ```bash
17
- wji asset upload trailer.mp4 --json # ≤500 MB, any length
18
- wji br set <postId> --trailer <assetId>
17
+ wji br set <postId> --trailer trailer.mp4 # uploads to VOD, ≤500 MB, any length
18
+ wji br set <postId> --trailer vod:<Vid> # reuse a video already in VOD
19
+ wji br set <postId> --trailer none # remove it
19
20
  ```
20
21
 
21
- There is no duration cap. The upload is rejected only when the file is over
22
- 500 MB.
22
+ The file goes straight to Backreel's VOD space (the same flow as episodes), so
23
+ viewers get adaptive streaming and the quality menu. There is no duration cap;
24
+ only files over 500 MB are refused. It plays after transcoding finishes,
25
+ usually within a minute or two (`br get` shows `processing` → `ready`).
26
+ Passing a WJI asset id still works but is the old way.
23
27
 
24
28
  ## Uploading episodes
25
29