@mevdragon/vidfarm-devcli 0.3.6 → 0.4.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/README.md +2 -0
- package/SKILL.director.md +85 -4
- package/SKILL.platform.md +3 -3
- package/dist/src/cli.js +857 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,4 +48,6 @@ npx -y @mevdragon/vidfarm-devcli <template_id>
|
|
|
48
48
|
|
|
49
49
|
Flags: `--port`, `--dir`, `--host`, `--fork`, `--api-key` (or `VIDFARM_API_KEY`), `--share`, `--refetch`. Use `dev-serve` as an explicit subcommand to skip the fetch step and just serve an existing directory.
|
|
50
50
|
|
|
51
|
+
Beyond the local editor loop, the devcli also wraps the **entire director REST flow** as 1:1 commands — `discover`, `inspiration-add`, `fork`, `decompose`, `snapshot`, `render`, `approve`, `schedule`, `posts`, `upload`/`download`, plus a raw `vidfarm api <METHOD> <path>` passthrough. Each maps to exactly one REST route and prints the prod frontend URL to open. Run `vidfarm --help` for the full surface, or see `SKILL.director.md` § "`vidfarm-devcli` — full command surface".
|
|
52
|
+
|
|
51
53
|
See `SKILL.platform.md` § "Local editor dev loop" for the request-rewrite / SSE reload flow.
|
package/SKILL.director.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vidfarm-director
|
|
3
|
-
description: Use Vidfarm as a director.
|
|
3
|
+
description: Use Vidfarm as a director. Browse/add inspiration videos, fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as a raw REST route and as a 1:1 `vidfarm-devcli` command.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Vidfarm Director
|
|
@@ -82,6 +82,16 @@ Each template exposes a public preview:
|
|
|
82
82
|
|
|
83
83
|
Directors don't edit templates directly. They fork.
|
|
84
84
|
|
|
85
|
+
### Add your own source (inspiration)
|
|
86
|
+
|
|
87
|
+
To bring a new viral video into the catalog as a **private** template you own, ingest its social URL:
|
|
88
|
+
|
|
89
|
+
- `POST /discover/templates { source_url, tagline?, notes? }` — accepts TikTok / YouTube / Instagram / Twitter-X URLs only. The video download runs async; the response is the inspiration record (`202`, `status: "processing"`). The card finalizes into a private `template_...` the next time `GET /discover/feed` is polled (the feed poll doubles as the completion check).
|
|
90
|
+
- `DELETE /discover/templates/:entryId` — remove a private inspiration/template you own (accepts either the `inspiration_...` or minted `template_...` id).
|
|
91
|
+
- `POST /api/v1/inspirations/:inspirationId/decompose { user_prompt? }` — AI-decompose an inspiration's downloaded video into scenes (requires a saved provider key; same 120s source cap as auto-decompose).
|
|
92
|
+
|
|
93
|
+
devcli: `vidfarm inspiration-add <url>`, `vidfarm inspiration-rm <id>`, `vidfarm inspiration-decompose <id>`, and `vidfarm discover` to browse.
|
|
94
|
+
|
|
85
95
|
## Fork a template
|
|
86
96
|
|
|
87
97
|
```
|
|
@@ -193,9 +203,9 @@ Use it whenever you need to know what the video says or shows: writing/translati
|
|
|
193
203
|
- **Editor chat (frontend AI)** exposes this as the optional `video_context` tool — the copilot calls it on demand.
|
|
194
204
|
- **Desktop agents (Claude Code / Codex)**: `vidfarm edit <template_id>` syncs the same payload to `video-context.json` on disk next to `composition.html` (use `--refetch` to refresh it after a new decompose), or fetch the route directly.
|
|
195
205
|
|
|
196
|
-
##
|
|
206
|
+
## Render (publish to MP4)
|
|
197
207
|
|
|
198
|
-
|
|
208
|
+
Rendering publishes the fork's current working state to an MP4 using shared Remotion Lambda. In the Trackpad Editor this is the **Render** button; the REST route is still named `/export` (the route name predates the UI rename — treat "Render", "publish", and `/export` as the same operation). The devcli exposes it as `vidfarm render <forkId> [--wait]`.
|
|
199
209
|
|
|
200
210
|
```
|
|
201
211
|
POST /api/v1/compositions/:forkId/export
|
|
@@ -214,6 +224,41 @@ Response includes `{ ok, renderId, status: "RUNNING" | "SUCCEEDED" | "FAILED", p
|
|
|
214
224
|
|
|
215
225
|
Every publish creates an immutable version snapshot at `versions/<N>/composition.html` and `versions/<N>/composition.json`.
|
|
216
226
|
|
|
227
|
+
## Approve a finished post
|
|
228
|
+
|
|
229
|
+
A render produces a bare MP4 URL. **Approving** wraps that MP4 (plus caption, title, pinned comment, and any carousel slides) into a shareable preview page — the phone-mockup page a human opens to review and copy the post.
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
POST /api/v1/approved/posts
|
|
233
|
+
Content-Type: application/json
|
|
234
|
+
|
|
235
|
+
{ "caption": "required", "title": "optional", "pinned_comment": "optional", "tracer": "optional",
|
|
236
|
+
"media": [ { "url": "https://.../output.mp4", "kind": "video", "role": "primary" } ] }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`caption` is required. `media[]` items take `{ url, kind?: "image"|"video"|..., role?: "primary"|"slide"|... }`. Response (`201`) is the approved post including **`share_url`** — the prod frontend page for previewing/sharing. Surface that URL to the user; it is the headline output of this step.
|
|
240
|
+
|
|
241
|
+
- `GET /api/v1/approved/posts` — list your approved posts
|
|
242
|
+
- `GET /api/v1/approved/posts/:postId` — read one (returns `share_url`, `download_zip_url`)
|
|
243
|
+
|
|
244
|
+
devcli: `vidfarm approve --video <mp4-url> --caption "..."` prints the `share_url` as a first-class openable link; `vidfarm posts` lists, `vidfarm post <id>` reads one.
|
|
245
|
+
|
|
246
|
+
## Schedule a post
|
|
247
|
+
|
|
248
|
+
Schedule an approved post to a connected destination channel (FlockPoster social account or email) at one ISO timestamp:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
POST /api/v1/approved/posts/:postId/schedules
|
|
252
|
+
Content-Type: application/json
|
|
253
|
+
|
|
254
|
+
{ "destination_type": "flockposter" | "email", "destination_id": "<channel or email>",
|
|
255
|
+
"scheduled_at": "2026-07-10T14:00:00Z", "timezone": "America/New_York", "additional_notes": "optional" }
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Minimum 10-minute lead time. Response (`201`) is the schedule record. Browse existing schedules with `GET /api/v1/approved/posts/:postId/schedules`.
|
|
259
|
+
|
|
260
|
+
devcli: `vidfarm schedule <postId> --at <iso> --to <destinationId> [--type flockposter|email]`, and `vidfarm schedules <postId>` to browse.
|
|
261
|
+
|
|
217
262
|
## Version history
|
|
218
263
|
|
|
219
264
|
```
|
|
@@ -300,7 +345,43 @@ For agents that operate Vidfarm headlessly, the typical loop is:
|
|
|
300
345
|
|
|
301
346
|
Send a stable `tracer` on export so retries are traceable and filterable in job history — but note that submission is **not idempotent**: every POST creates a new job (and a new charge) even with the same tracer. Do not blind-retry expensive submissions; check the render status first.
|
|
302
347
|
|
|
303
|
-
##
|
|
348
|
+
## `vidfarm-devcli` — full command surface
|
|
349
|
+
|
|
350
|
+
`@mevdragon/vidfarm-devcli` wraps the **entire director REST flow** as CLI commands. It is a thin shell over the REST API, not a second implementation: **every named command maps 1:1 to exactly one REST route**, and all it adds is fork auto-resolution, the `vidfarm-api-key` header, prod frontend links in the output, render-poll waiting, and the multi-step upload/download flows. Auth via `--api-key <key>` or `VIDFARM_API_KEY`.
|
|
351
|
+
|
|
352
|
+
**Raw REST vs devcli — your choice.** If you want full control, call the REST API directly (or use the raw passthrough `vidfarm api <METHOD> <path> [--data <json>] [--query k=v]`). If you want ergonomics + openable frontend URLs, use the named commands. They are interchangeable — pick per call.
|
|
353
|
+
|
|
354
|
+
| Command | REST route | Flow step |
|
|
355
|
+
|---|---|---|
|
|
356
|
+
| `vidfarm discover` | `GET /discover/feed` | browse inspiration/templates |
|
|
357
|
+
| `vidfarm inspiration-add <url>` | `POST /discover/templates` | add your own source |
|
|
358
|
+
| `vidfarm inspiration-rm <id>` | `DELETE /discover/templates/:id` | remove a private one |
|
|
359
|
+
| `vidfarm inspiration-decompose <id>` | `POST /api/v1/inspirations/:id/decompose` | AI-decompose an inspiration |
|
|
360
|
+
| `vidfarm fork <template_id>` | `POST /api/v1/compositions` | fork a template |
|
|
361
|
+
| `vidfarm decompose <forkId>` | `POST .../compositions/:forkId/auto-decompose` | split source into scenes |
|
|
362
|
+
| `vidfarm ghostcut <forkId>` | `GET .../compositions/:forkId/ghostcut` | subtitle-removal status |
|
|
363
|
+
| `vidfarm snapshot <forkId>` | `POST .../compositions/:forkId/versions` | save composition fork version |
|
|
364
|
+
| `vidfarm versions <forkId>` | `GET .../compositions/:forkId/versions` | list versions |
|
|
365
|
+
| `vidfarm render <forkId> [--wait]` | `POST .../compositions/:forkId/export` | render video to MP4 |
|
|
366
|
+
| `vidfarm render-status <forkId> <renderId>` | `GET .../compositions/:forkId/renders/:renderId` | poll a render |
|
|
367
|
+
| `vidfarm visibility <forkId> <private\|public>` | `PATCH .../compositions/:forkId/visibility` | set visibility |
|
|
368
|
+
| `vidfarm clone <forkId>` | `POST .../compositions/:forkId/clone` | clone a fork |
|
|
369
|
+
| `vidfarm share-link <forkId>` | `POST .../compositions/:forkId/share-links` | mint a share URL |
|
|
370
|
+
| `vidfarm approve --video <url> --caption "…"` | `POST /api/v1/approved/posts` | approve post (prints `share_url`) |
|
|
371
|
+
| `vidfarm posts` / `vidfarm post <id>` | `GET /api/v1/approved/posts[/:id]` | browse approved posts |
|
|
372
|
+
| `vidfarm schedule <postId> --at <iso> --to <dest>` | `POST /api/v1/approved/posts/:postId/schedules` | schedule a post |
|
|
373
|
+
| `vidfarm schedules <postId>` | `GET /api/v1/approved/posts/:postId/schedules` | browse scheduled posts |
|
|
374
|
+
| `vidfarm login <email>` / `vidfarm verify <email> <code>` | `POST /api/v1/user/request-otp` · `verify-otp` | get an API key |
|
|
375
|
+
| `vidfarm whoami` | `GET /api/v1/user/me` | who am I |
|
|
376
|
+
| `vidfarm provider-keys` / `vidfarm add-provider-key <p> <secret>` | `GET`·`POST /api/v1/user/me/provider-keys` | manage AI keys |
|
|
377
|
+
| `vidfarm upload <file>` | `POST /api/v1/user/me/temporary-files/upload` | upload → durable URL |
|
|
378
|
+
| `vidfarm download <url> [dest]` | (streams any URL to disk) | download media |
|
|
379
|
+
|
|
380
|
+
**Frontend URLs are first-class output.** Every command that has a human-openable page (editor, discover, approved-post preview, share link) prints that prod frontend URL as a highlighted line. `render --wait` polls to completion and prints the final MP4 URL; `approve` prints the approved-post `share_url`. Add `--json` to any command for pure JSON (agent-friendly, no banners).
|
|
381
|
+
|
|
382
|
+
**Uploads/downloads live in the devcli** because they are genuine multi-step / streaming flows: `upload` posts the file as multipart to the temporary-files route and prints the durable URL to drop into a composition or approved post; `download` streams any Vidfarm/media URL to disk.
|
|
383
|
+
|
|
384
|
+
## Local dev loop (edit on disk)
|
|
304
385
|
|
|
305
386
|
For iterating on a composition on disk (e.g. Claude Code editing HTML directly), pair the hosted editor with a local composition directory:
|
|
306
387
|
|
package/SKILL.platform.md
CHANGED
|
@@ -184,7 +184,7 @@ The editor is a single monolithic React component (~9k lines). It builds cleanly
|
|
|
184
184
|
|
|
185
185
|
Publish flow:
|
|
186
186
|
|
|
187
|
-
1. Client `POST /api/v1/compositions/:forkId/export` (the route is named `export`; the
|
|
187
|
+
1. Client `POST /api/v1/compositions/:forkId/export` (the route is named `export`; the editor button and devcli command are both **Render**; treat "render", "publish", and `/export` as the same operation)
|
|
188
188
|
2. API stamps `data-composition-id="<forkId>@v<N>"` into the composition HTML, PUTs the stamped HTML to working, and reads composition.json
|
|
189
189
|
3. API resolves the caller's provider keys (preflight check for required models)
|
|
190
190
|
4. API creates a `job` record and invokes the `hyperframes_render` primitive Lambda
|
|
@@ -353,7 +353,7 @@ Legacy but still active:
|
|
|
353
353
|
- Composition runtime script — `src/composition-runtime.ts`
|
|
354
354
|
- Trackpad Editor — `demo/src/HyperframesStudioEditor.tsx`
|
|
355
355
|
- Editor local-mode bootstrap — `demo/src/dev-mode.ts`
|
|
356
|
-
- Devcli (`vidfarm-devcli`) — `src/cli.ts` (edit-mode fetch + banner), `src/dev-serve.ts` (raw HTTP shim)
|
|
356
|
+
- Devcli (`vidfarm-devcli`) — `src/cli.ts` (edit-mode fetch + banner, the REST-wrapper commands, and the shared `apiRequest()` helper), `src/dev-serve.ts` (raw HTTP shim)
|
|
357
357
|
- Auto-decompose logic — `src/hyperframes/composition.ts` (time-slice), `src/services/hyperframes.ts` (smart)
|
|
358
358
|
- Publish flow — `src/app.ts` `publishCompositionToRenderer()`
|
|
359
359
|
- Ghostcut — `src/services/ghostcut.ts`
|
|
@@ -361,7 +361,7 @@ Legacy but still active:
|
|
|
361
361
|
|
|
362
362
|
## Local editor dev loop
|
|
363
363
|
|
|
364
|
-
`@mevdragon/vidfarm-devcli` (`src/cli.ts` + `src/dev-serve.ts`) pairs a local directory of composition files with the hosted Trackpad Editor so Claude Code, Codex, or a human can edit `composition.html` on disk and see instant preview reload in the browser.
|
|
364
|
+
`@mevdragon/vidfarm-devcli` (`src/cli.ts` + `src/dev-serve.ts`) has two roles. First, it pairs a local directory of composition files with the hosted Trackpad Editor so Claude Code, Codex, or a human can edit `composition.html` on disk and see instant preview reload in the browser (the `edit`/`publish`/`dev-serve` commands, detailed below). Second, it wraps the **entire director REST flow** as 1:1 CLI commands — `discover`, `inspiration-add`, `fork`, `decompose`, `snapshot`, `render` (→ `/export`), `approve`, `schedule`, `posts`/`schedules`, `upload`/`download`, `login`/`whoami`/`provider-keys`, plus a raw `api <METHOD> <path>` passthrough. Every wrapper routes through a single `apiRequest()` helper (auth + host + JSON), so it never duplicates route logic; the command↔route table and the raw-REST-vs-devcli guidance live in `SKILL.director.md`. Each command prints its prod frontend URL (editor / discover / approved-post `share_url`) as a first-class output, and `--json` gives pure JSON for agents.
|
|
365
365
|
|
|
366
366
|
```bash
|
|
367
367
|
# One-shot: fetch composition, start dev-serve, print editor URL.
|
package/dist/src/cli.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { parseArgs } from "node:util";
|
|
5
7
|
import { spawnSync } from "node:child_process";
|
|
6
8
|
import { Readable } from "node:stream";
|
|
@@ -11,34 +13,101 @@ import { pipeline } from "node:stream/promises";
|
|
|
11
13
|
// starts a local dev-serve, and prints the editor URL to open in the browser.
|
|
12
14
|
const DEFAULT_HOST = "https://vidfarm.cc";
|
|
13
15
|
const DEFAULT_PORT = 4321;
|
|
14
|
-
|
|
16
|
+
// Agent skill files the `update-skill` command can install from the live host
|
|
17
|
+
// (`GET /skill/<name>`), falling back to the copy bundled in this npm package.
|
|
18
|
+
const SKILL_TARGETS = {
|
|
19
|
+
"vidfarm-director": { route: "/skill/vidfarm-director", bundled: "SKILL.director.md" },
|
|
20
|
+
"vidfarm-platform": { route: "/skill/vidfarm-platform", bundled: "SKILL.platform.md" }
|
|
21
|
+
};
|
|
22
|
+
// Every command below is a thin wrapper over ONE Vidfarm REST call (the `→`
|
|
23
|
+
// line documents the exact route). The devcli adds nothing the raw API can't
|
|
24
|
+
// do — it just resolves your fork, sets the `vidfarm-api-key` header, prints
|
|
25
|
+
// the prod frontend URL to open, and handles the multi-step upload/download +
|
|
26
|
+
// render-polling flows that are awkward to do by hand. Prefer the raw REST API
|
|
27
|
+
// (or `vidfarm api <METHOD> <path>`) whenever you want full control; reach for
|
|
28
|
+
// the named commands when you want the ergonomics and the frontend links.
|
|
29
|
+
const HELP = `vidfarm-devcli — command-line bridge for the Vidfarm video studio
|
|
30
|
+
|
|
31
|
+
Every named command maps 1:1 to a single REST route (shown as → below). Use
|
|
32
|
+
raw REST (or \`vidfarm api\`) when you want control; use named commands for the
|
|
33
|
+
ergonomics + prod frontend links. Auth: --api-key <key> or VIDFARM_API_KEY.
|
|
15
34
|
|
|
16
35
|
Usage:
|
|
17
|
-
vidfarm
|
|
18
|
-
vidfarm
|
|
19
|
-
vidfarm
|
|
20
|
-
vidfarm-devcli --help Show this help
|
|
36
|
+
vidfarm <template_id> [opts] Start local editor session (default)
|
|
37
|
+
vidfarm <command> [args] [opts]
|
|
38
|
+
vidfarm --help
|
|
21
39
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
Local editor loop:
|
|
41
|
+
<template_id> [opts] Fetch composition + media, dev-serve, print editor URL
|
|
42
|
+
publish [template_id] Push local composition.html/json to your fork → PUT/PATCH composition + POST versions
|
|
43
|
+
dev-serve Serve an existing local dir only (no fetch)
|
|
44
|
+
|
|
45
|
+
Discover & inspiration (browse the viral-video catalog, add your own source):
|
|
46
|
+
discover List the template/inspiration feed → GET /discover/feed
|
|
47
|
+
inspiration-add <url> Ingest a TikTok/YT/IG/X video as a template → POST /discover/templates
|
|
48
|
+
inspiration-rm <id> Delete a private inspiration/template you own → DELETE /discover/templates/:id
|
|
49
|
+
inspiration-decompose <id> AI-decompose an inspiration into scenes → POST /api/v1/inspirations/:id/decompose
|
|
50
|
+
|
|
51
|
+
Composition lifecycle (fork → decompose → snapshot → render):
|
|
52
|
+
fork <template_id> Fork a template into an editable composition → POST /api/v1/compositions
|
|
53
|
+
decompose <forkId> Split the fork's source into scenes (smart) → POST .../compositions/:forkId/auto-decompose
|
|
54
|
+
ghostcut <forkId> Read subtitle-removal status (non-billing) → GET .../compositions/:forkId/ghostcut
|
|
55
|
+
versions <forkId> List immutable version snapshots → GET .../compositions/:forkId/versions
|
|
56
|
+
snapshot <forkId> Save the working state as a new version → POST .../compositions/:forkId/versions
|
|
57
|
+
render <forkId> Render the fork to MP4 (--wait to poll) → POST .../compositions/:forkId/export
|
|
58
|
+
render-status <forkId> <renderId> Poll one render job → GET .../compositions/:forkId/renders/:renderId
|
|
59
|
+
visibility <forkId> <private|public> Set fork visibility → PATCH .../compositions/:forkId/visibility
|
|
60
|
+
clone <forkId> Clone a fork as a new starting point → POST .../compositions/:forkId/clone
|
|
61
|
+
share-link <forkId> Mint a share-token URL for a fork → POST .../compositions/:forkId/share-links
|
|
62
|
+
|
|
63
|
+
Approve & schedule (publish a finished MP4 as a shareable post):
|
|
64
|
+
approve Approve media into a preview/share page → POST /api/v1/approved/posts
|
|
65
|
+
posts List your approved posts → GET /api/v1/approved/posts
|
|
66
|
+
post <postId> Read one approved post (prints share URL) → GET /api/v1/approved/posts/:postId
|
|
67
|
+
schedule <postId> Schedule an approved post to a channel → POST /api/v1/approved/posts/:postId/schedules
|
|
68
|
+
schedules <postId> Browse an approved post's schedules → GET /api/v1/approved/posts/:postId/schedules
|
|
69
|
+
|
|
70
|
+
Account:
|
|
71
|
+
login <email> Send an OTP code to email → POST /api/v1/user/request-otp
|
|
72
|
+
verify <email> <code> Verify the OTP, print a fresh API key → POST /api/v1/user/verify-otp
|
|
73
|
+
whoami Show the authenticated account → GET /api/v1/user/me
|
|
74
|
+
provider-keys List saved AI provider keys → GET /api/v1/user/me/provider-keys
|
|
75
|
+
add-provider-key <provider> <secret> Save an AI provider key → POST /api/v1/user/me/provider-keys
|
|
76
|
+
|
|
77
|
+
Agent skill (install the latest director skill so your AI agent can act):
|
|
78
|
+
update-skill Fetch latest SKILL.director.md → .claude/skills → GET /skill/vidfarm-director
|
|
79
|
+
--global Install into ~/.claude/skills (default: ./.claude/skills)
|
|
80
|
+
--dir <path> Install into a custom skills root
|
|
81
|
+
--platform Also install SKILL.platform.md (vidfarm-platform)
|
|
82
|
+
--print Print the skill to stdout instead of writing a file
|
|
83
|
+
|
|
84
|
+
Files (multi-step flows the devcli handles for you):
|
|
85
|
+
upload <file> Upload a local file, print its durable URL → POST /api/v1/user/me/temporary-files/upload
|
|
86
|
+
download <url> [dest] Stream any Vidfarm/media URL to disk
|
|
87
|
+
|
|
88
|
+
Escape hatch — call ANY route directly:
|
|
89
|
+
api <METHOD> <path> Raw REST call with auth + pretty errors
|
|
90
|
+
--data <json> JSON request body (or --data-file <path>)
|
|
91
|
+
--query k=v Repeatable query param
|
|
92
|
+
Examples:
|
|
93
|
+
vidfarm api GET /discover/feed
|
|
94
|
+
vidfarm api POST /api/v1/compositions --data '{"template_id":"template_..."}'
|
|
95
|
+
|
|
96
|
+
Common options (any command):
|
|
97
|
+
--host <url> Vidfarm host (default: ${DEFAULT_HOST})
|
|
27
98
|
--api-key <key> Vidfarm API key (or set VIDFARM_API_KEY)
|
|
28
99
|
--share <token> Share token for a shared fork (public read)
|
|
29
|
-
--
|
|
100
|
+
--json Print only the raw JSON response (agent-friendly)
|
|
30
101
|
|
|
31
|
-
|
|
102
|
+
Editor-mode options:
|
|
103
|
+
--port <n> Local dev-serve port (default: ${DEFAULT_PORT})
|
|
32
104
|
--dir <path> Local composition directory (default: .vidfarm/<template_id>)
|
|
33
|
-
--
|
|
34
|
-
--
|
|
35
|
-
--api-key <key> Vidfarm API key (or set VIDFARM_API_KEY) — required to write
|
|
36
|
-
--message <text> Optional message attached to the published version snapshot
|
|
37
|
-
--no-snapshot Only update the working copy; skip the version snapshot
|
|
105
|
+
--fork <id> Fork id (default: auto-resolve for the current user)
|
|
106
|
+
--refetch Overwrite local composition files with the latest remote copy
|
|
38
107
|
|
|
39
|
-
|
|
40
|
-
--
|
|
41
|
-
--
|
|
108
|
+
Publish-mode options:
|
|
109
|
+
--message <text> Message attached to the published version snapshot
|
|
110
|
+
--no-snapshot Only update the working copy; skip the version snapshot
|
|
42
111
|
`;
|
|
43
112
|
void main().catch((error) => {
|
|
44
113
|
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
@@ -51,25 +120,222 @@ async function main() {
|
|
|
51
120
|
return;
|
|
52
121
|
}
|
|
53
122
|
const command = argv[0];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
123
|
+
const rest = argv.slice(1);
|
|
124
|
+
switch (command) {
|
|
125
|
+
case "dev-serve": {
|
|
126
|
+
const { runDevServeCommand } = await import("./dev-serve.js");
|
|
127
|
+
await runDevServeCommand(rest);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
case "edit":
|
|
131
|
+
await runEditCommand(rest);
|
|
132
|
+
return;
|
|
133
|
+
case "publish":
|
|
134
|
+
await runPublishCommand(rest);
|
|
135
|
+
return;
|
|
136
|
+
// Raw escape hatch + all the 1:1 REST wrappers.
|
|
137
|
+
case "api":
|
|
138
|
+
await runApiCommand(rest);
|
|
139
|
+
return;
|
|
140
|
+
case "discover":
|
|
141
|
+
await runDiscoverCommand(rest);
|
|
142
|
+
return;
|
|
143
|
+
case "inspiration-add":
|
|
144
|
+
await runInspirationAddCommand(rest);
|
|
145
|
+
return;
|
|
146
|
+
case "inspiration-rm":
|
|
147
|
+
await runInspirationRemoveCommand(rest);
|
|
148
|
+
return;
|
|
149
|
+
case "inspiration-decompose":
|
|
150
|
+
await runInspirationDecomposeCommand(rest);
|
|
151
|
+
return;
|
|
152
|
+
case "fork":
|
|
153
|
+
await runForkCommand(rest);
|
|
154
|
+
return;
|
|
155
|
+
case "decompose":
|
|
156
|
+
await runDecomposeCommand(rest);
|
|
157
|
+
return;
|
|
158
|
+
case "ghostcut":
|
|
159
|
+
await runGhostcutCommand(rest);
|
|
160
|
+
return;
|
|
161
|
+
case "versions":
|
|
162
|
+
await runVersionsCommand(rest);
|
|
163
|
+
return;
|
|
164
|
+
case "snapshot":
|
|
165
|
+
await runSnapshotCommand(rest);
|
|
166
|
+
return;
|
|
167
|
+
case "render":
|
|
168
|
+
await runRenderCommand(rest);
|
|
169
|
+
return;
|
|
170
|
+
case "render-status":
|
|
171
|
+
await runRenderStatusCommand(rest);
|
|
172
|
+
return;
|
|
173
|
+
case "visibility":
|
|
174
|
+
await runVisibilityCommand(rest);
|
|
175
|
+
return;
|
|
176
|
+
case "clone":
|
|
177
|
+
await runCloneCommand(rest);
|
|
178
|
+
return;
|
|
179
|
+
case "share-link":
|
|
180
|
+
await runShareLinkCommand(rest);
|
|
181
|
+
return;
|
|
182
|
+
case "approve":
|
|
183
|
+
await runApproveCommand(rest);
|
|
184
|
+
return;
|
|
185
|
+
case "posts":
|
|
186
|
+
await runPostsCommand(rest);
|
|
187
|
+
return;
|
|
188
|
+
case "post":
|
|
189
|
+
await runPostCommand(rest);
|
|
190
|
+
return;
|
|
191
|
+
case "schedule":
|
|
192
|
+
await runScheduleCommand(rest);
|
|
193
|
+
return;
|
|
194
|
+
case "schedules":
|
|
195
|
+
await runSchedulesCommand(rest);
|
|
196
|
+
return;
|
|
197
|
+
case "login":
|
|
198
|
+
await runLoginCommand(rest);
|
|
199
|
+
return;
|
|
200
|
+
case "verify":
|
|
201
|
+
await runVerifyCommand(rest);
|
|
202
|
+
return;
|
|
203
|
+
case "whoami":
|
|
204
|
+
await runWhoamiCommand(rest);
|
|
205
|
+
return;
|
|
206
|
+
case "update-skill":
|
|
207
|
+
case "skill":
|
|
208
|
+
await runUpdateSkillCommand(rest);
|
|
209
|
+
return;
|
|
210
|
+
case "provider-keys":
|
|
211
|
+
await runProviderKeysCommand(rest);
|
|
212
|
+
return;
|
|
213
|
+
case "add-provider-key":
|
|
214
|
+
await runAddProviderKeyCommand(rest);
|
|
215
|
+
return;
|
|
216
|
+
case "upload":
|
|
217
|
+
await runUploadCommand(rest);
|
|
218
|
+
return;
|
|
219
|
+
case "download":
|
|
220
|
+
await runDownloadCommand(rest);
|
|
221
|
+
return;
|
|
222
|
+
default:
|
|
223
|
+
if (!command.startsWith("-")) {
|
|
224
|
+
// Positional template id → default `edit` command.
|
|
225
|
+
await runEditCommand(argv);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
throw new Error(`Unknown option before command: ${command}\n\n${HELP}`);
|
|
58
229
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
230
|
+
}
|
|
231
|
+
// ── REST plumbing shared by every wrapper command ───────────────────────────
|
|
232
|
+
// One code path for auth + host + query + JSON. This is what makes the devcli a
|
|
233
|
+
// thin shell over the REST API rather than a second implementation: the named
|
|
234
|
+
// commands below just assemble a method/path/body and hand it here.
|
|
235
|
+
const FRONTEND = "\x1b[1m\x1b[36m"; // bold cyan — reserved for openable URLs
|
|
236
|
+
const BOLD = "\x1b[1m";
|
|
237
|
+
const DIM = "\x1b[2m";
|
|
238
|
+
const GREEN = "\x1b[32m";
|
|
239
|
+
const RED = "\x1b[31m";
|
|
240
|
+
const RESET = "\x1b[0m";
|
|
241
|
+
async function apiRequest(input) {
|
|
242
|
+
const url = new URL(input.path, input.host);
|
|
243
|
+
for (const [key, value] of Object.entries(input.query ?? {})) {
|
|
244
|
+
if (value !== undefined && value !== null && value !== "")
|
|
245
|
+
url.searchParams.set(key, value);
|
|
62
246
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
247
|
+
const headers = { ...buildAuthHeaders(input.auth), accept: "application/json" };
|
|
248
|
+
let body;
|
|
249
|
+
if (input.body !== undefined) {
|
|
250
|
+
headers["content-type"] = "application/json";
|
|
251
|
+
body = JSON.stringify(input.body);
|
|
252
|
+
}
|
|
253
|
+
const res = await fetch(url, { method: input.method.toUpperCase(), headers, body });
|
|
254
|
+
const text = await res.text();
|
|
255
|
+
let json = null;
|
|
256
|
+
try {
|
|
257
|
+
json = text ? JSON.parse(text) : null;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
json = null;
|
|
66
261
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
262
|
+
return { status: res.status, ok: res.ok, json, text };
|
|
263
|
+
}
|
|
264
|
+
// Named commands should fail loudly (unlike the read-tolerant edit fetch).
|
|
265
|
+
function assertApiOk(result, action) {
|
|
266
|
+
if (result.ok)
|
|
267
|
+
return;
|
|
268
|
+
const detail = result.json && typeof result.json === "object" && typeof result.json.error === "string"
|
|
269
|
+
? ` — ${result.json.error}`
|
|
270
|
+
: result.text
|
|
271
|
+
? ` — ${result.text.slice(0, 300)}`
|
|
272
|
+
: "";
|
|
273
|
+
const hint = result.status === 401 || result.status === 403
|
|
274
|
+
? " Check --api-key / VIDFARM_API_KEY and that you own this resource."
|
|
275
|
+
: result.status === 402
|
|
276
|
+
? " The account is not on a paid plan (or the wallet is empty)."
|
|
277
|
+
: result.status === 429
|
|
278
|
+
? " Rate-limited; wait and retry."
|
|
279
|
+
: "";
|
|
280
|
+
throw new Error(`${action} failed: ${result.status}${detail}.${hint}`);
|
|
281
|
+
}
|
|
282
|
+
// Resolve host + auth + output mode shared by every wrapper command. Adding
|
|
283
|
+
// these to a parseArgs `options` block keeps the flags consistent everywhere.
|
|
284
|
+
function commonOptions() {
|
|
285
|
+
return {
|
|
286
|
+
host: { type: "string", default: DEFAULT_HOST },
|
|
287
|
+
"api-key": { type: "string" },
|
|
288
|
+
share: { type: "string" },
|
|
289
|
+
json: { type: "boolean", default: false }
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function commonContext(values) {
|
|
293
|
+
return {
|
|
294
|
+
host: trimTrailingSlash(String(values.host ?? DEFAULT_HOST)),
|
|
295
|
+
auth: {
|
|
296
|
+
apiKey: values["api-key"] ?? process.env.VIDFARM_API_KEY,
|
|
297
|
+
shareToken: values.share
|
|
298
|
+
},
|
|
299
|
+
json: Boolean(values.json)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function printJson(value) {
|
|
303
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
304
|
+
}
|
|
305
|
+
// Print the JSON result and, when not in --json mode, a highlighted list of the
|
|
306
|
+
// prod frontend URLs a human can open. Frontend links are a first-class output.
|
|
307
|
+
function emitResult(result, json, frontend) {
|
|
308
|
+
if (json) {
|
|
309
|
+
printJson(result.json ?? result.text);
|
|
70
310
|
return;
|
|
71
311
|
}
|
|
72
|
-
|
|
312
|
+
printJson(result.json ?? result.text);
|
|
313
|
+
const links = (frontend ?? []).filter((entry) => Boolean(entry[1]));
|
|
314
|
+
if (links.length > 0) {
|
|
315
|
+
console.log("");
|
|
316
|
+
for (const [label, url] of links) {
|
|
317
|
+
console.log(` ${BOLD}${label}${RESET} ${FRONTEND}${url}${RESET}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Frontend URL builders — these are the pages a director opens in a browser.
|
|
322
|
+
function editorFrontendUrl(host, templateId, forkId) {
|
|
323
|
+
const base = `${host}/editor/${encodeURIComponent(templateId)}`;
|
|
324
|
+
return forkId ? `${base}?fork=${encodeURIComponent(forkId)}` : base;
|
|
325
|
+
}
|
|
326
|
+
function discoverFrontendUrl(host) {
|
|
327
|
+
return `${host}/discover`;
|
|
328
|
+
}
|
|
329
|
+
// Small arg helper: split "k=v" pairs for --query.
|
|
330
|
+
function collectKeyValues(entries) {
|
|
331
|
+
const out = {};
|
|
332
|
+
for (const entry of entries ?? []) {
|
|
333
|
+
const eq = entry.indexOf("=");
|
|
334
|
+
if (eq === -1)
|
|
335
|
+
throw new Error(`--query expects k=v, got: ${entry}`);
|
|
336
|
+
out[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
73
339
|
}
|
|
74
340
|
async function runEditCommand(argv) {
|
|
75
341
|
const parsed = parseArgs({
|
|
@@ -578,4 +844,561 @@ function printEditorBanner(input) {
|
|
|
578
844
|
function trimTrailingSlash(value) {
|
|
579
845
|
return value.replace(/\/+$/, "");
|
|
580
846
|
}
|
|
847
|
+
function sleep(ms) {
|
|
848
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
849
|
+
}
|
|
850
|
+
// ── Raw escape hatch: call any REST route ───────────────────────────────────
|
|
851
|
+
async function runApiCommand(argv) {
|
|
852
|
+
const parsed = parseArgs({
|
|
853
|
+
args: argv,
|
|
854
|
+
allowPositionals: true,
|
|
855
|
+
options: { ...commonOptions(), data: { type: "string" }, "data-file": { type: "string" }, query: { type: "string", multiple: true } }
|
|
856
|
+
});
|
|
857
|
+
const method = parsed.positionals[0];
|
|
858
|
+
const routePath = parsed.positionals[1];
|
|
859
|
+
if (!method || !routePath) {
|
|
860
|
+
throw new Error(`api requires <METHOD> <path>.\n\nExample: vidfarm api GET /discover/feed`);
|
|
861
|
+
}
|
|
862
|
+
const ctx = commonContext(parsed.values);
|
|
863
|
+
let body;
|
|
864
|
+
if (parsed.values["data-file"]) {
|
|
865
|
+
body = JSON.parse(readFileSync(path.resolve(process.cwd(), String(parsed.values["data-file"])), "utf8"));
|
|
866
|
+
}
|
|
867
|
+
else if (parsed.values.data) {
|
|
868
|
+
body = JSON.parse(String(parsed.values.data));
|
|
869
|
+
}
|
|
870
|
+
const result = await apiRequest({
|
|
871
|
+
method,
|
|
872
|
+
host: ctx.host,
|
|
873
|
+
path: routePath,
|
|
874
|
+
auth: ctx.auth,
|
|
875
|
+
query: collectKeyValues(parsed.values.query),
|
|
876
|
+
body
|
|
877
|
+
});
|
|
878
|
+
if (!ctx.json) {
|
|
879
|
+
const color = result.ok ? GREEN : RED;
|
|
880
|
+
console.log(`${color}${method.toUpperCase()} ${routePath} → ${result.status}${RESET}`);
|
|
881
|
+
}
|
|
882
|
+
printJson(result.json ?? result.text);
|
|
883
|
+
if (!result.ok)
|
|
884
|
+
process.exitCode = 1;
|
|
885
|
+
}
|
|
886
|
+
// ── Discover & inspiration ──────────────────────────────────────────────────
|
|
887
|
+
async function runDiscoverCommand(argv) {
|
|
888
|
+
const parsed = parseArgs({ args: argv, allowPositionals: false, options: { ...commonOptions(), limit: { type: "string" } } });
|
|
889
|
+
const ctx = commonContext(parsed.values);
|
|
890
|
+
const result = await apiRequest({ method: "GET", host: ctx.host, path: "/discover/feed", auth: ctx.auth, query: { limit: parsed.values.limit } });
|
|
891
|
+
assertApiOk(result, "discover");
|
|
892
|
+
emitResult(result, ctx.json, [["Browse ", discoverFrontendUrl(ctx.host)]]);
|
|
893
|
+
}
|
|
894
|
+
async function runInspirationAddCommand(argv) {
|
|
895
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), tagline: { type: "string" }, notes: { type: "string" } } });
|
|
896
|
+
const sourceUrl = parsed.positionals[0];
|
|
897
|
+
if (!sourceUrl)
|
|
898
|
+
throw new Error("inspiration-add requires a video URL (TikTok/YouTube/Instagram/X).");
|
|
899
|
+
const ctx = commonContext(parsed.values);
|
|
900
|
+
const result = await apiRequest({
|
|
901
|
+
method: "POST",
|
|
902
|
+
host: ctx.host,
|
|
903
|
+
path: "/discover/templates",
|
|
904
|
+
auth: ctx.auth,
|
|
905
|
+
body: { source_url: sourceUrl, tagline: parsed.values.tagline, notes: parsed.values.notes }
|
|
906
|
+
});
|
|
907
|
+
assertApiOk(result, "inspiration-add");
|
|
908
|
+
emitResult(result, ctx.json, [["Discover ", discoverFrontendUrl(ctx.host)]]);
|
|
909
|
+
}
|
|
910
|
+
async function runInspirationRemoveCommand(argv) {
|
|
911
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: commonOptions() });
|
|
912
|
+
const entryId = parsed.positionals[0];
|
|
913
|
+
if (!entryId)
|
|
914
|
+
throw new Error("inspiration-rm requires an inspiration_/template_ id.");
|
|
915
|
+
const ctx = commonContext(parsed.values);
|
|
916
|
+
const result = await apiRequest({ method: "DELETE", host: ctx.host, path: `/discover/templates/${encodeURIComponent(entryId)}`, auth: ctx.auth });
|
|
917
|
+
assertApiOk(result, "inspiration-rm");
|
|
918
|
+
emitResult(result, ctx.json);
|
|
919
|
+
}
|
|
920
|
+
async function runInspirationDecomposeCommand(argv) {
|
|
921
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), prompt: { type: "string" } } });
|
|
922
|
+
const inspirationId = parsed.positionals[0];
|
|
923
|
+
if (!inspirationId)
|
|
924
|
+
throw new Error("inspiration-decompose requires an inspiration id.");
|
|
925
|
+
const ctx = commonContext(parsed.values);
|
|
926
|
+
const result = await apiRequest({
|
|
927
|
+
method: "POST",
|
|
928
|
+
host: ctx.host,
|
|
929
|
+
path: `/api/v1/inspirations/${encodeURIComponent(inspirationId)}/decompose`,
|
|
930
|
+
auth: ctx.auth,
|
|
931
|
+
body: { user_prompt: parsed.values.prompt ?? null }
|
|
932
|
+
});
|
|
933
|
+
assertApiOk(result, "inspiration-decompose");
|
|
934
|
+
emitResult(result, ctx.json);
|
|
935
|
+
}
|
|
936
|
+
// ── Composition lifecycle ───────────────────────────────────────────────────
|
|
937
|
+
async function runForkCommand(argv) {
|
|
938
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), title: { type: "string" } } });
|
|
939
|
+
const templateId = parsed.positionals[0];
|
|
940
|
+
if (!templateId)
|
|
941
|
+
throw new Error("fork requires a template id (template_...).");
|
|
942
|
+
const ctx = commonContext(parsed.values);
|
|
943
|
+
const result = await apiRequest({
|
|
944
|
+
method: "POST",
|
|
945
|
+
host: ctx.host,
|
|
946
|
+
path: "/api/v1/compositions",
|
|
947
|
+
auth: ctx.auth,
|
|
948
|
+
body: { template_id: templateId, title: parsed.values.title }
|
|
949
|
+
});
|
|
950
|
+
assertApiOk(result, "fork");
|
|
951
|
+
const forkId = result.json?.fork_id;
|
|
952
|
+
const tId = result.json?.template_id ?? templateId;
|
|
953
|
+
emitResult(result, ctx.json, [["Open editor ", forkId ? editorFrontendUrl(ctx.host, tId, forkId) : null]]);
|
|
954
|
+
}
|
|
955
|
+
async function runDecomposeCommand(argv) {
|
|
956
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), mode: { type: "string", default: "smart" }, prompt: { type: "string" } } });
|
|
957
|
+
const forkId = parsed.positionals[0];
|
|
958
|
+
if (!forkId)
|
|
959
|
+
throw new Error("decompose requires a fork id.");
|
|
960
|
+
const ctx = commonContext(parsed.values);
|
|
961
|
+
const result = await apiRequest({
|
|
962
|
+
method: "POST",
|
|
963
|
+
host: ctx.host,
|
|
964
|
+
path: `/api/v1/compositions/${encodeURIComponent(forkId)}/auto-decompose`,
|
|
965
|
+
auth: ctx.auth,
|
|
966
|
+
body: { mode: parsed.values.mode, user_prompt: parsed.values.prompt ?? null }
|
|
967
|
+
});
|
|
968
|
+
assertApiOk(result, "decompose");
|
|
969
|
+
if (!ctx.json && result.json?.ghostcut_pending) {
|
|
970
|
+
console.log(`${DIM}GhostCut subtitle-removal is running in the background — poll \`vidfarm api POST /api/v1/compositions/${forkId}/ghostcut-poll\` or GET .../ghostcut until done.${RESET}`);
|
|
971
|
+
}
|
|
972
|
+
emitResult(result, ctx.json);
|
|
973
|
+
}
|
|
974
|
+
async function runGhostcutCommand(argv) {
|
|
975
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: commonOptions() });
|
|
976
|
+
const forkId = parsed.positionals[0];
|
|
977
|
+
if (!forkId)
|
|
978
|
+
throw new Error("ghostcut requires a fork id.");
|
|
979
|
+
const ctx = commonContext(parsed.values);
|
|
980
|
+
const result = await apiRequest({ method: "GET", host: ctx.host, path: `/api/v1/compositions/${encodeURIComponent(forkId)}/ghostcut`, auth: ctx.auth });
|
|
981
|
+
assertApiOk(result, "ghostcut");
|
|
982
|
+
emitResult(result, ctx.json);
|
|
983
|
+
}
|
|
984
|
+
async function runVersionsCommand(argv) {
|
|
985
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), limit: { type: "string" } } });
|
|
986
|
+
const forkId = parsed.positionals[0];
|
|
987
|
+
if (!forkId)
|
|
988
|
+
throw new Error("versions requires a fork id.");
|
|
989
|
+
const ctx = commonContext(parsed.values);
|
|
990
|
+
const result = await apiRequest({ method: "GET", host: ctx.host, path: `/api/v1/compositions/${encodeURIComponent(forkId)}/versions`, auth: ctx.auth, query: { limit: parsed.values.limit } });
|
|
991
|
+
assertApiOk(result, "versions");
|
|
992
|
+
emitResult(result, ctx.json);
|
|
993
|
+
}
|
|
994
|
+
async function runSnapshotCommand(argv) {
|
|
995
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), message: { type: "string" } } });
|
|
996
|
+
const forkId = parsed.positionals[0];
|
|
997
|
+
if (!forkId)
|
|
998
|
+
throw new Error("snapshot requires a fork id.");
|
|
999
|
+
const ctx = commonContext(parsed.values);
|
|
1000
|
+
const result = await apiRequest({ method: "POST", host: ctx.host, path: `/api/v1/compositions/${encodeURIComponent(forkId)}/versions`, auth: ctx.auth, body: { message: parsed.values.message ?? null } });
|
|
1001
|
+
assertApiOk(result, "snapshot");
|
|
1002
|
+
emitResult(result, ctx.json);
|
|
1003
|
+
}
|
|
1004
|
+
async function runRenderCommand(argv) {
|
|
1005
|
+
const parsed = parseArgs({
|
|
1006
|
+
args: argv,
|
|
1007
|
+
allowPositionals: true,
|
|
1008
|
+
options: { ...commonOptions(), title: { type: "string" }, tracer: { type: "string" }, wait: { type: "boolean", default: false } }
|
|
1009
|
+
});
|
|
1010
|
+
const forkId = parsed.positionals[0];
|
|
1011
|
+
if (!forkId)
|
|
1012
|
+
throw new Error("render requires a fork id.");
|
|
1013
|
+
const ctx = commonContext(parsed.values);
|
|
1014
|
+
const result = await apiRequest({
|
|
1015
|
+
method: "POST",
|
|
1016
|
+
host: ctx.host,
|
|
1017
|
+
path: `/api/v1/compositions/${encodeURIComponent(forkId)}/export`,
|
|
1018
|
+
auth: ctx.auth,
|
|
1019
|
+
body: { title: parsed.values.title, tracer: parsed.values.tracer }
|
|
1020
|
+
});
|
|
1021
|
+
assertApiOk(result, "render");
|
|
1022
|
+
const renderId = result.json?.renderId;
|
|
1023
|
+
if (!parsed.values.wait || !renderId) {
|
|
1024
|
+
if (!ctx.json && renderId) {
|
|
1025
|
+
console.log(`${DIM}Poll with: vidfarm render-status ${forkId} ${renderId} (or add --wait next time).${RESET}`);
|
|
1026
|
+
}
|
|
1027
|
+
emitResult(result, ctx.json, [["Rendered MP4 ", result.json?.outputUrl]]);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
// --wait: poll the render job until it settles.
|
|
1031
|
+
if (!ctx.json)
|
|
1032
|
+
console.log(`${DIM}Rendering ${renderId}… polling every 5s.${RESET}`);
|
|
1033
|
+
let last = result;
|
|
1034
|
+
for (;;) {
|
|
1035
|
+
await sleep(5000);
|
|
1036
|
+
last = await apiRequest({ method: "GET", host: ctx.host, path: `/api/v1/compositions/${encodeURIComponent(forkId)}/renders/${encodeURIComponent(renderId)}`, auth: ctx.auth });
|
|
1037
|
+
assertApiOk(last, "render-status");
|
|
1038
|
+
const status = String(last.json?.status ?? "");
|
|
1039
|
+
if (!ctx.json) {
|
|
1040
|
+
const pct = Math.round((Number(last.json?.progress ?? 0)) * 100);
|
|
1041
|
+
console.log(`${DIM} ${status} ${pct}%${RESET}`);
|
|
1042
|
+
}
|
|
1043
|
+
if (status === "SUCCEEDED" || status === "FAILED")
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
emitResult(last, ctx.json, [["Rendered MP4 ", last.json?.outputUrl]]);
|
|
1047
|
+
if (String(last.json?.status) === "FAILED")
|
|
1048
|
+
process.exitCode = 1;
|
|
1049
|
+
}
|
|
1050
|
+
async function runRenderStatusCommand(argv) {
|
|
1051
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: commonOptions() });
|
|
1052
|
+
const forkId = parsed.positionals[0];
|
|
1053
|
+
const renderId = parsed.positionals[1];
|
|
1054
|
+
if (!forkId || !renderId)
|
|
1055
|
+
throw new Error("render-status requires <forkId> <renderId>.");
|
|
1056
|
+
const ctx = commonContext(parsed.values);
|
|
1057
|
+
const result = await apiRequest({ method: "GET", host: ctx.host, path: `/api/v1/compositions/${encodeURIComponent(forkId)}/renders/${encodeURIComponent(renderId)}`, auth: ctx.auth });
|
|
1058
|
+
assertApiOk(result, "render-status");
|
|
1059
|
+
emitResult(result, ctx.json, [["Rendered MP4 ", result.json?.outputUrl]]);
|
|
1060
|
+
}
|
|
1061
|
+
async function runVisibilityCommand(argv) {
|
|
1062
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: commonOptions() });
|
|
1063
|
+
const forkId = parsed.positionals[0];
|
|
1064
|
+
const visibility = parsed.positionals[1];
|
|
1065
|
+
if (!forkId || !visibility)
|
|
1066
|
+
throw new Error("visibility requires <forkId> <private|public>.");
|
|
1067
|
+
const ctx = commonContext(parsed.values);
|
|
1068
|
+
const result = await apiRequest({ method: "PATCH", host: ctx.host, path: `/api/v1/compositions/${encodeURIComponent(forkId)}/visibility`, auth: ctx.auth, body: { visibility } });
|
|
1069
|
+
assertApiOk(result, "visibility");
|
|
1070
|
+
const templateId = result.json?.template_id;
|
|
1071
|
+
emitResult(result, ctx.json, templateId ? [["Open editor ", editorFrontendUrl(ctx.host, templateId, forkId)]] : undefined);
|
|
1072
|
+
}
|
|
1073
|
+
async function runCloneCommand(argv) {
|
|
1074
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), version: { type: "string" }, title: { type: "string" } } });
|
|
1075
|
+
const forkId = parsed.positionals[0];
|
|
1076
|
+
if (!forkId)
|
|
1077
|
+
throw new Error("clone requires a fork id.");
|
|
1078
|
+
const ctx = commonContext(parsed.values);
|
|
1079
|
+
const result = await apiRequest({
|
|
1080
|
+
method: "POST",
|
|
1081
|
+
host: ctx.host,
|
|
1082
|
+
path: `/api/v1/compositions/${encodeURIComponent(forkId)}/clone`,
|
|
1083
|
+
auth: ctx.auth,
|
|
1084
|
+
body: { parent_version: parsed.values.version, title: parsed.values.title }
|
|
1085
|
+
});
|
|
1086
|
+
assertApiOk(result, "clone");
|
|
1087
|
+
const newForkId = result.json?.fork_id;
|
|
1088
|
+
const templateId = result.json?.template_id;
|
|
1089
|
+
emitResult(result, ctx.json, newForkId && templateId ? [["Open editor ", editorFrontendUrl(ctx.host, templateId, newForkId)]] : undefined);
|
|
1090
|
+
}
|
|
1091
|
+
async function runShareLinkCommand(argv) {
|
|
1092
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), role: { type: "string", default: "viewer" }, expires: { type: "string" } } });
|
|
1093
|
+
const forkId = parsed.positionals[0];
|
|
1094
|
+
if (!forkId)
|
|
1095
|
+
throw new Error("share-link requires a fork id.");
|
|
1096
|
+
const ctx = commonContext(parsed.values);
|
|
1097
|
+
const result = await apiRequest({
|
|
1098
|
+
method: "POST",
|
|
1099
|
+
host: ctx.host,
|
|
1100
|
+
path: `/api/v1/compositions/${encodeURIComponent(forkId)}/share-links`,
|
|
1101
|
+
auth: ctx.auth,
|
|
1102
|
+
body: { role: parsed.values.role, expires_at: parsed.values.expires ?? null }
|
|
1103
|
+
});
|
|
1104
|
+
assertApiOk(result, "share-link");
|
|
1105
|
+
emitResult(result, ctx.json, [["Share ", result.json?.share_url]]);
|
|
1106
|
+
}
|
|
1107
|
+
// ── Approve & schedule ──────────────────────────────────────────────────────
|
|
1108
|
+
async function runApproveCommand(argv) {
|
|
1109
|
+
const parsed = parseArgs({
|
|
1110
|
+
args: argv,
|
|
1111
|
+
allowPositionals: false,
|
|
1112
|
+
options: {
|
|
1113
|
+
...commonOptions(),
|
|
1114
|
+
video: { type: "string" },
|
|
1115
|
+
media: { type: "string", multiple: true },
|
|
1116
|
+
caption: { type: "string" },
|
|
1117
|
+
title: { type: "string" },
|
|
1118
|
+
pinned: { type: "string" },
|
|
1119
|
+
tracer: { type: "string" }
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
const ctx = commonContext(parsed.values);
|
|
1123
|
+
const caption = parsed.values.caption;
|
|
1124
|
+
if (!caption)
|
|
1125
|
+
throw new Error("approve requires --caption <text>.");
|
|
1126
|
+
const media = [];
|
|
1127
|
+
if (parsed.values.video)
|
|
1128
|
+
media.push({ url: parsed.values.video, kind: "video", role: "primary" });
|
|
1129
|
+
for (const url of parsed.values.media ?? [])
|
|
1130
|
+
media.push({ url });
|
|
1131
|
+
if (media.length === 0) {
|
|
1132
|
+
console.warn(`${DIM}[vidfarm] no --video / --media given; approving a text-only post.${RESET}`);
|
|
1133
|
+
}
|
|
1134
|
+
const result = await apiRequest({
|
|
1135
|
+
method: "POST",
|
|
1136
|
+
host: ctx.host,
|
|
1137
|
+
path: "/api/v1/approved/posts",
|
|
1138
|
+
auth: ctx.auth,
|
|
1139
|
+
body: {
|
|
1140
|
+
caption,
|
|
1141
|
+
title: parsed.values.title,
|
|
1142
|
+
pinned_comment: parsed.values.pinned,
|
|
1143
|
+
tracer: parsed.values.tracer,
|
|
1144
|
+
media
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
assertApiOk(result, "approve");
|
|
1148
|
+
// The approved-post preview page is the headline output: it's the prod
|
|
1149
|
+
// frontend URL a human opens to see/share the finished post.
|
|
1150
|
+
emitResult(result, ctx.json, [["Preview post ", result.json?.post?.share_url]]);
|
|
1151
|
+
}
|
|
1152
|
+
async function runPostsCommand(argv) {
|
|
1153
|
+
const parsed = parseArgs({ args: argv, allowPositionals: false, options: { ...commonOptions(), limit: { type: "string" }, tracer: { type: "string" } } });
|
|
1154
|
+
const ctx = commonContext(parsed.values);
|
|
1155
|
+
const result = await apiRequest({ method: "GET", host: ctx.host, path: "/api/v1/approved/posts", auth: ctx.auth, query: { limit: parsed.values.limit, tracer: parsed.values.tracer } });
|
|
1156
|
+
assertApiOk(result, "posts");
|
|
1157
|
+
emitResult(result, ctx.json);
|
|
1158
|
+
}
|
|
1159
|
+
async function runPostCommand(argv) {
|
|
1160
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: commonOptions() });
|
|
1161
|
+
const postId = parsed.positionals[0];
|
|
1162
|
+
if (!postId)
|
|
1163
|
+
throw new Error("post requires a post id.");
|
|
1164
|
+
const ctx = commonContext(parsed.values);
|
|
1165
|
+
const result = await apiRequest({ method: "GET", host: ctx.host, path: `/api/v1/approved/posts/${encodeURIComponent(postId)}`, auth: ctx.auth });
|
|
1166
|
+
assertApiOk(result, "post");
|
|
1167
|
+
emitResult(result, ctx.json, [["Preview post ", result.json?.post?.share_url]]);
|
|
1168
|
+
}
|
|
1169
|
+
async function runScheduleCommand(argv) {
|
|
1170
|
+
const parsed = parseArgs({
|
|
1171
|
+
args: argv,
|
|
1172
|
+
allowPositionals: true,
|
|
1173
|
+
options: { ...commonOptions(), at: { type: "string" }, to: { type: "string" }, type: { type: "string", default: "flockposter" }, timezone: { type: "string" }, notes: { type: "string" } }
|
|
1174
|
+
});
|
|
1175
|
+
const postId = parsed.positionals[0];
|
|
1176
|
+
if (!postId)
|
|
1177
|
+
throw new Error("schedule requires a post id.");
|
|
1178
|
+
if (!parsed.values.at)
|
|
1179
|
+
throw new Error("schedule requires --at <ISO 8601 timestamp>.");
|
|
1180
|
+
if (!parsed.values.to)
|
|
1181
|
+
throw new Error("schedule requires --to <destination_id> (a FlockPoster channel or email).");
|
|
1182
|
+
const ctx = commonContext(parsed.values);
|
|
1183
|
+
const result = await apiRequest({
|
|
1184
|
+
method: "POST",
|
|
1185
|
+
host: ctx.host,
|
|
1186
|
+
path: `/api/v1/approved/posts/${encodeURIComponent(postId)}/schedules`,
|
|
1187
|
+
auth: ctx.auth,
|
|
1188
|
+
body: {
|
|
1189
|
+
destination_type: parsed.values.type,
|
|
1190
|
+
destination_id: parsed.values.to,
|
|
1191
|
+
scheduled_at: parsed.values.at,
|
|
1192
|
+
timezone: parsed.values.timezone,
|
|
1193
|
+
additional_notes: parsed.values.notes
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
assertApiOk(result, "schedule");
|
|
1197
|
+
emitResult(result, ctx.json);
|
|
1198
|
+
}
|
|
1199
|
+
async function runSchedulesCommand(argv) {
|
|
1200
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: commonOptions() });
|
|
1201
|
+
const postId = parsed.positionals[0];
|
|
1202
|
+
if (!postId)
|
|
1203
|
+
throw new Error("schedules requires a post id.");
|
|
1204
|
+
const ctx = commonContext(parsed.values);
|
|
1205
|
+
const result = await apiRequest({ method: "GET", host: ctx.host, path: `/api/v1/approved/posts/${encodeURIComponent(postId)}/schedules`, auth: ctx.auth });
|
|
1206
|
+
assertApiOk(result, "schedules");
|
|
1207
|
+
emitResult(result, ctx.json);
|
|
1208
|
+
}
|
|
1209
|
+
// ── Account ─────────────────────────────────────────────────────────────────
|
|
1210
|
+
async function runLoginCommand(argv) {
|
|
1211
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: commonOptions() });
|
|
1212
|
+
const email = parsed.positionals[0];
|
|
1213
|
+
if (!email)
|
|
1214
|
+
throw new Error("login requires an email address.");
|
|
1215
|
+
const ctx = commonContext(parsed.values);
|
|
1216
|
+
const result = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/user/request-otp", auth: ctx.auth, body: { email } });
|
|
1217
|
+
assertApiOk(result, "login");
|
|
1218
|
+
if (!ctx.json)
|
|
1219
|
+
console.log(`${GREEN}OTP requested for ${email}. Run: vidfarm verify ${email} <code>${RESET}`);
|
|
1220
|
+
emitResult(result, ctx.json);
|
|
1221
|
+
}
|
|
1222
|
+
async function runVerifyCommand(argv) {
|
|
1223
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), name: { type: "string" } } });
|
|
1224
|
+
const email = parsed.positionals[0];
|
|
1225
|
+
const code = parsed.positionals[1];
|
|
1226
|
+
if (!email || !code)
|
|
1227
|
+
throw new Error("verify requires <email> <code>.");
|
|
1228
|
+
const ctx = commonContext(parsed.values);
|
|
1229
|
+
const result = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/user/verify-otp", auth: ctx.auth, body: { email, code, name: parsed.values.name } });
|
|
1230
|
+
assertApiOk(result, "verify");
|
|
1231
|
+
const apiKey = result.json?.apiKey;
|
|
1232
|
+
if (!ctx.json && apiKey) {
|
|
1233
|
+
console.log(`${GREEN}${BOLD}Save this API key:${RESET} ${apiKey}`);
|
|
1234
|
+
console.log(`${DIM}export VIDFARM_API_KEY=${apiKey}${RESET}`);
|
|
1235
|
+
}
|
|
1236
|
+
emitResult(result, ctx.json);
|
|
1237
|
+
}
|
|
1238
|
+
async function runWhoamiCommand(argv) {
|
|
1239
|
+
const parsed = parseArgs({ args: argv, allowPositionals: false, options: commonOptions() });
|
|
1240
|
+
const ctx = commonContext(parsed.values);
|
|
1241
|
+
const result = await apiRequest({ method: "GET", host: ctx.host, path: "/api/v1/user/me", auth: ctx.auth });
|
|
1242
|
+
assertApiOk(result, "whoami");
|
|
1243
|
+
emitResult(result, ctx.json);
|
|
1244
|
+
}
|
|
1245
|
+
async function runProviderKeysCommand(argv) {
|
|
1246
|
+
const parsed = parseArgs({ args: argv, allowPositionals: false, options: commonOptions() });
|
|
1247
|
+
const ctx = commonContext(parsed.values);
|
|
1248
|
+
const result = await apiRequest({ method: "GET", host: ctx.host, path: "/api/v1/user/me/provider-keys", auth: ctx.auth });
|
|
1249
|
+
assertApiOk(result, "provider-keys");
|
|
1250
|
+
emitResult(result, ctx.json);
|
|
1251
|
+
}
|
|
1252
|
+
async function runAddProviderKeyCommand(argv) {
|
|
1253
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), label: { type: "string" } } });
|
|
1254
|
+
const provider = parsed.positionals[0];
|
|
1255
|
+
const secret = parsed.positionals[1];
|
|
1256
|
+
if (!provider || !secret)
|
|
1257
|
+
throw new Error("add-provider-key requires <provider> <secret> (provider: openai|gemini|openrouter|perplexity).");
|
|
1258
|
+
const ctx = commonContext(parsed.values);
|
|
1259
|
+
const result = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/user/me/provider-keys", auth: ctx.auth, body: { provider, secret, label: parsed.values.label } });
|
|
1260
|
+
assertApiOk(result, "add-provider-key");
|
|
1261
|
+
emitResult(result, ctx.json);
|
|
1262
|
+
}
|
|
1263
|
+
// ── Files ───────────────────────────────────────────────────────────────────
|
|
1264
|
+
// Upload is a genuine multi-step flow, so the devcli earns its keep here: it
|
|
1265
|
+
// posts the file as multipart/form-data to the temporary-files route and prints
|
|
1266
|
+
// the durable URL you can drop into a composition or an approved post.
|
|
1267
|
+
async function runUploadCommand(argv) {
|
|
1268
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), folder: { type: "string" } } });
|
|
1269
|
+
const filePath = parsed.positionals[0];
|
|
1270
|
+
if (!filePath)
|
|
1271
|
+
throw new Error("upload requires a local file path.");
|
|
1272
|
+
const ctx = commonContext(parsed.values);
|
|
1273
|
+
const abs = path.resolve(process.cwd(), filePath);
|
|
1274
|
+
if (!existsSync(abs))
|
|
1275
|
+
throw new Error(`No such file: ${abs}`);
|
|
1276
|
+
const buffer = readFileSync(abs);
|
|
1277
|
+
const form = new FormData();
|
|
1278
|
+
form.append("file", new Blob([buffer]), path.basename(abs));
|
|
1279
|
+
if (parsed.values.folder)
|
|
1280
|
+
form.append("folder_path", String(parsed.values.folder));
|
|
1281
|
+
const res = await fetch(new URL("/api/v1/user/me/temporary-files/upload", ctx.host), {
|
|
1282
|
+
method: "POST",
|
|
1283
|
+
headers: buildAuthHeaders(ctx.auth),
|
|
1284
|
+
body: form
|
|
1285
|
+
});
|
|
1286
|
+
const text = await res.text();
|
|
1287
|
+
let json = null;
|
|
1288
|
+
try {
|
|
1289
|
+
json = text ? JSON.parse(text) : null;
|
|
1290
|
+
}
|
|
1291
|
+
catch {
|
|
1292
|
+
json = null;
|
|
1293
|
+
}
|
|
1294
|
+
const result = { status: res.status, ok: res.ok, json, text };
|
|
1295
|
+
assertApiOk(result, "upload");
|
|
1296
|
+
emitResult(result, ctx.json, [["Durable URL ", result.json?.file?.view_url ?? result.json?.file?.s3_url]]);
|
|
1297
|
+
}
|
|
1298
|
+
async function runDownloadCommand(argv) {
|
|
1299
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: commonOptions() });
|
|
1300
|
+
const url = parsed.positionals[0];
|
|
1301
|
+
if (!url)
|
|
1302
|
+
throw new Error("download requires a URL.");
|
|
1303
|
+
const ctx = commonContext(parsed.values);
|
|
1304
|
+
const dest = parsed.positionals[1]
|
|
1305
|
+
? path.resolve(process.cwd(), parsed.positionals[1])
|
|
1306
|
+
: path.resolve(process.cwd(), path.basename(new URL(url).pathname) || "download.bin");
|
|
1307
|
+
const res = await fetch(url, { headers: buildAuthHeaders(ctx.auth) });
|
|
1308
|
+
if (!res.ok || !res.body)
|
|
1309
|
+
throw new Error(`download failed: ${res.status} ${res.statusText} (${url}).`);
|
|
1310
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
|
|
1311
|
+
const bytes = safeSize(dest);
|
|
1312
|
+
if (ctx.json) {
|
|
1313
|
+
printJson({ ok: true, url, path: dest, bytes });
|
|
1314
|
+
}
|
|
1315
|
+
else {
|
|
1316
|
+
console.log(`${GREEN}Downloaded ${formatBytes(bytes)} → ${dest}${RESET}`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
// ── Agent skill ───────────────────────────────────────────────────────────────
|
|
1320
|
+
// Install the latest director skill onto disk as a Claude Code / agent skill so
|
|
1321
|
+
// the user's AI agent can read SKILL.director.md and act. We pull the freshest
|
|
1322
|
+
// copy from the live host (`GET /skill/<name>`) and fall back to the copy that
|
|
1323
|
+
// shipped inside this npm package when offline. Written as `<skill>/SKILL.md`.
|
|
1324
|
+
// Walk up from this module's directory to find a bundled skill file. Handles
|
|
1325
|
+
// both the published layout (dist/src/cli.js → package root) and running from
|
|
1326
|
+
// source (src/cli.ts → repo root).
|
|
1327
|
+
function locateBundledSkill(filename) {
|
|
1328
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
1329
|
+
for (let i = 0; i < 6; i += 1) {
|
|
1330
|
+
const candidate = path.join(dir, filename);
|
|
1331
|
+
if (existsSync(candidate))
|
|
1332
|
+
return candidate;
|
|
1333
|
+
const parent = path.dirname(dir);
|
|
1334
|
+
if (parent === dir)
|
|
1335
|
+
break;
|
|
1336
|
+
dir = parent;
|
|
1337
|
+
}
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
async function fetchSkillContents(host, target) {
|
|
1341
|
+
try {
|
|
1342
|
+
const res = await fetch(new URL(target.route, host));
|
|
1343
|
+
if (res.ok) {
|
|
1344
|
+
const contents = await res.text();
|
|
1345
|
+
if (contents.trim())
|
|
1346
|
+
return { contents, source: "remote" };
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
catch {
|
|
1350
|
+
// fall through to the bundled copy below
|
|
1351
|
+
}
|
|
1352
|
+
const bundledPath = locateBundledSkill(target.bundled);
|
|
1353
|
+
if (bundledPath) {
|
|
1354
|
+
return { contents: readFileSync(bundledPath, "utf8"), source: "bundled" };
|
|
1355
|
+
}
|
|
1356
|
+
throw new Error(`Could not fetch ${target.bundled} from ${host} and no bundled copy was found.`);
|
|
1357
|
+
}
|
|
1358
|
+
async function runUpdateSkillCommand(argv) {
|
|
1359
|
+
const parsed = parseArgs({
|
|
1360
|
+
args: argv,
|
|
1361
|
+
allowPositionals: false,
|
|
1362
|
+
options: {
|
|
1363
|
+
...commonOptions(),
|
|
1364
|
+
global: { type: "boolean", default: false },
|
|
1365
|
+
dir: { type: "string" },
|
|
1366
|
+
platform: { type: "boolean", default: false },
|
|
1367
|
+
print: { type: "boolean", default: false }
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
const ctx = commonContext(parsed.values);
|
|
1371
|
+
const skillNames = ["vidfarm-director", ...(parsed.values.platform ? ["vidfarm-platform"] : [])];
|
|
1372
|
+
if (parsed.values.print) {
|
|
1373
|
+
for (const name of skillNames) {
|
|
1374
|
+
const { contents } = await fetchSkillContents(ctx.host, SKILL_TARGETS[name]);
|
|
1375
|
+
process.stdout.write(contents.endsWith("\n") ? contents : `${contents}\n`);
|
|
1376
|
+
}
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
const skillsRoot = parsed.values.dir
|
|
1380
|
+
? path.resolve(process.cwd(), String(parsed.values.dir))
|
|
1381
|
+
: parsed.values.global
|
|
1382
|
+
? path.join(homedir(), ".claude", "skills")
|
|
1383
|
+
: path.resolve(process.cwd(), ".claude", "skills");
|
|
1384
|
+
const written = [];
|
|
1385
|
+
for (const name of skillNames) {
|
|
1386
|
+
const { contents, source } = await fetchSkillContents(ctx.host, SKILL_TARGETS[name]);
|
|
1387
|
+
const destDir = path.join(skillsRoot, name);
|
|
1388
|
+
mkdirSync(destDir, { recursive: true });
|
|
1389
|
+
const destPath = path.join(destDir, "SKILL.md");
|
|
1390
|
+
writeFileSync(destPath, contents, "utf8");
|
|
1391
|
+
written.push({ skill: name, path: destPath, source, bytes: Buffer.byteLength(contents, "utf8") });
|
|
1392
|
+
}
|
|
1393
|
+
if (ctx.json) {
|
|
1394
|
+
printJson({ ok: true, skillsRoot, installed: written });
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
for (const entry of written) {
|
|
1398
|
+
const tag = entry.source === "bundled" ? " (offline — bundled copy)" : "";
|
|
1399
|
+
console.log(`${GREEN}Installed ${entry.skill} skill${tag} → ${entry.path}${RESET}`);
|
|
1400
|
+
}
|
|
1401
|
+
console.log("");
|
|
1402
|
+
console.log(` ${BOLD}Your AI agent can now use the "vidfarm-director" skill.${RESET}`);
|
|
1403
|
+
}
|
|
581
1404
|
//# sourceMappingURL=cli.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mevdragon/vidfarm-devcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Local bridge for the Vidfarm Trackpad Editor. Point it at a template id, edit composition.html on disk (Claude Code, Codex, etc.), preview live in the browser at https://vidfarm.cc/editor/.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|