@shotstack/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/SKILL.md +238 -0
- package/dist/shotstack.js +26 -0
- package/package.json +37 -0
- package/references/asset-library.md +64 -0
- package/references/fonts.md +124 -0
- package/references/rich-caption.md +222 -0
- package/references/svg.md +109 -0
- package/references/timeline.md +140 -0
- package/references/troubleshooting.md +108 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Shotstack CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for the [Shotstack](https://shotstack.io) video rendering API. Built for humans and AI coding agents.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
shotstack render template.json
|
|
7
|
+
shotstack status <render-id> --watch
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
# npm (global)
|
|
14
|
+
npm i -g @shotstack/cli
|
|
15
|
+
|
|
16
|
+
# one-shot via npx
|
|
17
|
+
npx @shotstack/cli render template.json
|
|
18
|
+
|
|
19
|
+
# one-shot via bun (~100x faster than npx for cached invocations)
|
|
20
|
+
bunx @shotstack/cli render template.json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
All three install paths use the same `@shotstack/cli` package on npm.
|
|
24
|
+
|
|
25
|
+
## Authentication
|
|
26
|
+
|
|
27
|
+
Set your API key as an environment variable:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
export SHOTSTACK_API_KEY=sk_...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Get a key at <https://shotstack.io>.
|
|
34
|
+
|
|
35
|
+
## Environments
|
|
36
|
+
|
|
37
|
+
Pick the API environment with `--env` or `SHOTSTACK_ENV`. Defaults to `v1` (production).
|
|
38
|
+
|
|
39
|
+
| `--env` | Endpoint |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `stage` | `https://api.shotstack.io/edit/stage` |
|
|
42
|
+
| `v1` (default) | `https://api.shotstack.io/edit/v1` |
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
shotstack render template.json --env stage
|
|
46
|
+
SHOTSTACK_ENV=stage shotstack render template.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Commands
|
|
50
|
+
|
|
51
|
+
### `shotstack render <file>`
|
|
52
|
+
|
|
53
|
+
Submits a Shotstack Edit JSON to the render API. Returns a render ID.
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
shotstack render my-template.json
|
|
57
|
+
shotstack render my-template.json --output json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `shotstack status <id>`
|
|
61
|
+
|
|
62
|
+
Polls the render status. Use `--watch` to poll continuously until the render completes.
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
shotstack status 01ja7-x8m2k-...
|
|
66
|
+
shotstack status 01ja7-x8m2k-... --watch
|
|
67
|
+
shotstack status 01ja7-x8m2k-... --output json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `shotstack feedback`
|
|
71
|
+
|
|
72
|
+
Opens a pre-filled GitHub issue with a sanitised dossier of your last 5 CLI invocations (render IDs, errors, exit codes). API keys and signed URLs are stripped at write time. You review and submit in your browser; nothing is transmitted automatically. Inspect the log at `~/.shotstack/log.jsonl`.
|
|
73
|
+
|
|
74
|
+
## Output
|
|
75
|
+
|
|
76
|
+
Default is human-readable. Pass `--output json` for machine-readable output. Exit codes:
|
|
77
|
+
|
|
78
|
+
- `0` success
|
|
79
|
+
- `1` permanent error (4xx, validation, missing API key)
|
|
80
|
+
- `2` transient/retryable error (5xx, network)
|
|
81
|
+
|
|
82
|
+
## For AI agents
|
|
83
|
+
|
|
84
|
+
This repo ships a [`SKILL.md`](./SKILL.md) at the root, following the [Agent Skills open standard](https://agentskills.io). Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, and 50+ other agents.
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
npx skills add shotstack/shotstack-cli
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The skill loads at session start (~50-100 tokens metadata) and teaches the agent three things `--help` doesn't:
|
|
91
|
+
|
|
92
|
+
- Always pass `--output json` when piping
|
|
93
|
+
- Use `shotstack status --watch` instead of writing a polling loop
|
|
94
|
+
- Fetch the current timeline schema from <https://shotstack.io/docs/llms.txt> before submitting
|
|
95
|
+
|
|
96
|
+
## Telemetry
|
|
97
|
+
|
|
98
|
+
Every API request includes two headers so we can split CLI traffic in dashboards:
|
|
99
|
+
|
|
100
|
+
- `x-shotstack-origin: cli`
|
|
101
|
+
- `x-shotstack-environment: stage | v1`
|
|
102
|
+
|
|
103
|
+
No other data is sent. The CLI never phones home; it only talks to the Shotstack API.
|
|
104
|
+
|
|
105
|
+
## Licence
|
|
106
|
+
|
|
107
|
+
Apache 2.0.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shotstack
|
|
3
|
+
description: |
|
|
4
|
+
Render video and poll render status via the Shotstack API.
|
|
5
|
+
Use when generating clips, building automated video pipelines, or orchestrating
|
|
6
|
+
cloud video renders from a script, agent, or CI workflow.
|
|
7
|
+
NOT for: real-time streaming, live broadcasting, or in-browser editing UIs.
|
|
8
|
+
license: Apache-2.0
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Shotstack CLI
|
|
12
|
+
|
|
13
|
+
Two commands for the Shotstack video rendering API. `render` submits a timeline JSON and returns a render ID; `status` polls a render until done.
|
|
14
|
+
|
|
15
|
+
## Authentication
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
export SHOTSTACK_API_KEY=sk_...
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Get a key at <https://dashboard.shotstack.io>. Without a key, every command exits with code 1.
|
|
22
|
+
|
|
23
|
+
## Environments
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
--env dev → https://api.shotstack.io/edit/dev
|
|
27
|
+
--env stage → https://api.shotstack.io/edit/stage (test credits, free)
|
|
28
|
+
--env v1 → https://api.shotstack.io/edit/v1 (production, default)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use `--env stage` for experimentation. Stage is free; v1 charges real credits. Override with `SHOTSTACK_ENV` env var for the session.
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
# Submit. Use --output json so the ID is parseable.
|
|
37
|
+
shotstack render template.json --output json --env stage
|
|
38
|
+
# → {"id":"01ja7-x8m2k-39rzv-cmvxve"}
|
|
39
|
+
|
|
40
|
+
# Poll until terminal state.
|
|
41
|
+
shotstack status 01ja7-x8m2k-39rzv-cmvxve --watch --env stage
|
|
42
|
+
# → done https://shotstack-api-stage-output.s3.amazonaws.com/.../out.mp4
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Three CLI rules
|
|
46
|
+
|
|
47
|
+
1. **Pipe → `--output json`.** Default output is human-readable. When parsing programmatically or piping to another command, always pass `--output json`. The text format is not stable across versions.
|
|
48
|
+
|
|
49
|
+
2. **Use `--watch`, not a polling loop.** `shotstack status <id> --watch` exits when the render reaches `done` (exit 0) or `failed` (exit 1). Don't write `while true; do ...; sleep 3; done`.
|
|
50
|
+
|
|
51
|
+
3. **Fetch the current docs before generating timeline JSON.** The Shotstack API evolves; LLM training data is often stale. Pull <https://shotstack.io/docs/guide/llms-full.txt> for the current schema and examples before composing a timeline from scratch.
|
|
52
|
+
|
|
53
|
+
## Authoring timeline JSON
|
|
54
|
+
|
|
55
|
+
These are the conventions agents most often get wrong. Read this section before generating any timeline JSON.
|
|
56
|
+
|
|
57
|
+
### Before composing JSON: check the schema
|
|
58
|
+
|
|
59
|
+
Don't invent property names or enum values. The Shotstack schema is published — fetch one of these before composing JSON from scratch:
|
|
60
|
+
|
|
61
|
+
- <https://shotstack.io/docs/api/api.bundled.json> — single-file JSON Schema. Machine-validatable; load it once and validate locally instead of round-tripping the API.
|
|
62
|
+
- <https://shotstack.io/docs/api/> — interactive HTML reference. Fastest for human scanning.
|
|
63
|
+
- <https://shotstack.io/docs/guide/llms-full.txt> — single-file LLM-friendly version of the full guide + reference.
|
|
64
|
+
- <https://github.com/shotstack/oas-api-definition/tree/main/schemas> — raw OpenAPI YAML, source of truth.
|
|
65
|
+
|
|
66
|
+
CSS naming conventions (`alignment`, `vertical: "center"`) **do not** apply. The spec uses precise names that often differ from web/CSS instincts:
|
|
67
|
+
|
|
68
|
+
| You'd guess (wrong) | API uses (right) |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `alignment` | `align` |
|
|
71
|
+
| `align.vertical: "center"` | `align.vertical: "middle"` |
|
|
72
|
+
| `font.name` | `font.family` |
|
|
73
|
+
| `duration` | `length` |
|
|
74
|
+
| `transitions: [...]` (array) | `transition: { in, out }` (object) |
|
|
75
|
+
| `fit: "cover"` (CSS instinct: scale+crop maintaining aspect) | `fit: "crop"` — Shotstack's `cover` STRETCHES without maintaining aspect ratio |
|
|
76
|
+
|
|
77
|
+
When the API rejects a property, the error message names the field — fix and retry. Don't guess twice.
|
|
78
|
+
|
|
79
|
+
### Track ordering is REVERSED
|
|
80
|
+
|
|
81
|
+
`timeline.tracks` is an array. **The first element (`tracks[0]`) is the TOP layer; the last element is the BOTTOM layer.** This is opposite to most z-index conventions.
|
|
82
|
+
|
|
83
|
+
> "Tracks are layered on top of each other in the same order they are added to the array with the top most track layered over the top of those below it." — [Shotstack docs](https://shotstack.io/docs/guide/architecting-an-application/guidelines/)
|
|
84
|
+
|
|
85
|
+
Practical rule: **put captions, overlays, and titles in early tracks; put video/image backgrounds in later tracks.**
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"timeline": {
|
|
90
|
+
"tracks": [
|
|
91
|
+
{ "clips": [/* TOP — captions */] },
|
|
92
|
+
{ "clips": [/* MIDDLE — title overlay */] },
|
|
93
|
+
{ "clips": [/* BOTTOM — background video */] }
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Smart clip strings
|
|
100
|
+
|
|
101
|
+
These string values are accepted in addition to numbers:
|
|
102
|
+
|
|
103
|
+
| Where | Value | Meaning |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `clip.start` | `"auto"` | Start when the previous clip on the same track finishes |
|
|
106
|
+
| `clip.start` | `"alias://<name>"` | Inherit start from another clip with `alias: "<name>"` |
|
|
107
|
+
| `clip.length` | `"auto"` | Play for the full duration of the asset |
|
|
108
|
+
| `clip.length` | `"end"` | Play until the end of the timeline |
|
|
109
|
+
| `clip.length` | `"alias://<name>"` | Inherit length from another clip |
|
|
110
|
+
|
|
111
|
+
The `alias://` protocol is also used in `rich-caption` `src` to auto-transcribe a referenced audio/video clip — see `references/rich-caption.md`.
|
|
112
|
+
|
|
113
|
+
### Public HTTPS URLs only
|
|
114
|
+
|
|
115
|
+
All asset `src` URLs must be publicly accessible HTTPS. **No local file paths, no `data:` URIs, no signed URLs that expire mid-render.** The render workers fetch assets from the public internet.
|
|
116
|
+
|
|
117
|
+
For test renders without your own assets, use the placeholder library at <https://shotstack-assets.s3.amazonaws.com/> — see `references/asset-library.md`.
|
|
118
|
+
|
|
119
|
+
### Don't overlap clips on the same track
|
|
120
|
+
|
|
121
|
+
Clips on the same track must not have overlapping `start`/`length` ranges. Overlapping clips flicker because the engine can't decide which to display. Put parallel content on separate tracks.
|
|
122
|
+
|
|
123
|
+
### Output resolution
|
|
124
|
+
|
|
125
|
+
Pick `output.resolution` (preset) OR `output.size.width`+`output.size.height` (custom):
|
|
126
|
+
|
|
127
|
+
| Preset | Pixels @ fps |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `preview` | 512×288 @ 15 |
|
|
130
|
+
| `mobile` | 640×360 @ 25 |
|
|
131
|
+
| `sd` | 1024×576 @ 25 |
|
|
132
|
+
| `hd` | 1280×720 @ 25 (default) |
|
|
133
|
+
| `1080` | 1920×1080 @ 25 |
|
|
134
|
+
|
|
135
|
+
Custom sizes must be divisible by 2.
|
|
136
|
+
|
|
137
|
+
## Asset types
|
|
138
|
+
|
|
139
|
+
The render API supports many asset types. Use only the **current** ones; the deprecated ones still parse but should not be used in new templates.
|
|
140
|
+
|
|
141
|
+
### Current
|
|
142
|
+
|
|
143
|
+
| Type | Purpose |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `video` | Video file (mp4, mov, webm). |
|
|
146
|
+
| `image` | Static image (jpg, png). |
|
|
147
|
+
| `audio` | Audio clip placed at a specific time on the timeline. |
|
|
148
|
+
| `rich-text` | Styled text overlay with full typography control. **Use this instead of `text`/`html`/`title`.** |
|
|
149
|
+
| `svg` | Vector graphics from raw SVG markup. See `references/svg.md`. |
|
|
150
|
+
| `rich-caption` | Word-level animated captions sourced from audio, video, or subtitle files. See `references/rich-caption.md`. |
|
|
151
|
+
| `luma` | Luma matte for masking effects. |
|
|
152
|
+
| `image-to-video` | **AI**: animate a still image into a short video clip. Billed per generation. |
|
|
153
|
+
| `text-to-image` | **AI**: generate an image from a text prompt. Billed per generation. |
|
|
154
|
+
|
|
155
|
+
`timeline.soundtrack` is a separate top-level field (not an asset type) for a single background music track. `timeline.fonts[]` is a separate field for custom font URLs.
|
|
156
|
+
|
|
157
|
+
### Deprecated — do not use
|
|
158
|
+
|
|
159
|
+
`text`, `title`, `caption`, `html`, `shape`. They still parse but produce inferior output. Replace with:
|
|
160
|
+
|
|
161
|
+
| If you'd use… | Use instead |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `text` or `title` | `rich-text` |
|
|
164
|
+
| `caption` | `rich-caption` |
|
|
165
|
+
| `html` | `rich-text` |
|
|
166
|
+
| `shape` | `svg` with `<rect>`, `<circle>`, `<polygon>` etc. |
|
|
167
|
+
|
|
168
|
+
### AI-generated assets
|
|
169
|
+
|
|
170
|
+
`image-to-video` and `text-to-image` are billed per generation **even when invoked through the sandbox stage endpoint** (which is otherwise free). They are async — the render submits the AI job and waits. Renders containing AI assets take longer.
|
|
171
|
+
|
|
172
|
+
## Fonts
|
|
173
|
+
|
|
174
|
+
Prefer **custom Google Fonts via `timeline.fonts[]`** over the built-in font list. The Studio SDK exposes ~400 Google Fonts. See `references/fonts.md` for the URL pattern and the built-in fallback list.
|
|
175
|
+
|
|
176
|
+
Per the [Shotstack docs](https://shotstack.io/docs/guide/architecting-an-application/rich-text/#custom-fonts), custom fonts give you the full Google Fonts catalogue with predictable rendering. System fonts like `Arial`, `Helvetica`, and `Times New Roman` are NOT installed and will fail with "Font not found".
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"timeline": {
|
|
181
|
+
"fonts": [
|
|
182
|
+
{ "src": "https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm45xW5rygbi49c.ttf" }
|
|
183
|
+
],
|
|
184
|
+
"tracks": [
|
|
185
|
+
{
|
|
186
|
+
"clips": [{
|
|
187
|
+
"asset": {
|
|
188
|
+
"type": "rich-text",
|
|
189
|
+
"text": "Hello",
|
|
190
|
+
"font": { "family": "JTUSjIg1_i6t8kCHKm45xW5rygbi49c", "size": 60, "color": "#ffffff" }
|
|
191
|
+
},
|
|
192
|
+
"start": 0, "length": 3
|
|
193
|
+
}]
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The `font.family` value is the **font file basename** (without `.ttf`/`.otf`).
|
|
201
|
+
|
|
202
|
+
## Top 5 mistakes
|
|
203
|
+
|
|
204
|
+
1. **Reverse track order.** `tracks[0]` is the TOP layer, not the bottom. Captions go in early tracks; backgrounds go in late tracks.
|
|
205
|
+
2. **System fonts.** `Arial`, `Helvetica`, `Times New Roman`, etc. are not installed. Use Google Fonts via `timeline.fonts[]` (preferred) or one of the built-in fonts in `references/fonts.md`.
|
|
206
|
+
3. **Captions fill the whole frame.** A `rich-caption` clip without `width`, `height`, and `fit: "none"` covers the entire output. Use a named preset from `references/rich-caption.md`.
|
|
207
|
+
4. **`<text>` inside an SVG asset.** Raw `<text>` is unsupported. Use a `rich-text` asset for any text content; reserve SVG for shapes only.
|
|
208
|
+
5. **Composing custom caption styles when presets exist.** The five named presets (Nico, Kai, Kapow, Lovely Little Lychee, Rizz) cover the common styles. Use one verbatim from `references/rich-caption.md` unless the user asks for something specific.
|
|
209
|
+
|
|
210
|
+
## Exit codes
|
|
211
|
+
|
|
212
|
+
| Code | Meaning |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `0` | Success |
|
|
215
|
+
| `1` | Permanent error — validation, missing key, 4xx, render `failed` |
|
|
216
|
+
| `2` | Transient error — 5xx, network, timeout. Safe to retry. |
|
|
217
|
+
|
|
218
|
+
## References
|
|
219
|
+
|
|
220
|
+
Authoritative sources, in order of preference:
|
|
221
|
+
|
|
222
|
+
- `shotstack --help` and `shotstack <command> --help` — current CLI flag listing
|
|
223
|
+
- <https://shotstack.io/docs/api/api.bundled.json> — JSON Schema for the render API (machine-validatable)
|
|
224
|
+
- <https://shotstack.io/docs/guide/llms-full.txt> — full API docs in LLM-friendly single file
|
|
225
|
+
- <https://github.com/shotstack/oas-api-definition/tree/main/schemas> — raw OpenAPI YAML, source of truth for property names and enums
|
|
226
|
+
- <https://shotstack.io/docs/api/> — interactive HTML reference
|
|
227
|
+
- <https://github.com/shotstack/shotstack-cli> — CLI source
|
|
228
|
+
|
|
229
|
+
This skill ships sub-references for the gnarly bits:
|
|
230
|
+
|
|
231
|
+
- [`references/timeline.md`](references/timeline.md) — track layering, transitions, soundtrack vs audio
|
|
232
|
+
- [`references/rich-caption.md`](references/rich-caption.md) — sizing per resolution, default style, the 5 named presets, alias pattern
|
|
233
|
+
- [`references/svg.md`](references/svg.md) — required attrs, supported elements
|
|
234
|
+
- [`references/fonts.md`](references/fonts.md) — built-in fonts, Google Fonts URL pattern, custom-font workflow
|
|
235
|
+
- [`references/asset-library.md`](references/asset-library.md) — placeholder videos, images, music
|
|
236
|
+
- [`references/troubleshooting.md`](references/troubleshooting.md) — common errors and fixes
|
|
237
|
+
|
|
238
|
+
If this skill and `--help` ever disagree, trust `--help`. If this skill and `llms-full.txt` ever disagree, trust `llms-full.txt`.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{createRequire as AE}from"node:module";var WE=Object.create;var{getPrototypeOf:DE,defineProperty:m,getOwnPropertyNames:FE}=Object;var kE=Object.prototype.hasOwnProperty;var VE=(E,f,R)=>{R=E!=null?WE(DE(E)):{};let $=f||!E||!E.__esModule?m(R,"default",{value:E,enumerable:!0}):R;for(let S of FE(E))if(!kE.call($,S))m($,S,{get:()=>E[S],enumerable:!0});return $};var X=(E,f)=>()=>(f||E((f={exports:{}}).exports,f),f.exports);var Q=AE(import.meta.url);var K=X((xE)=>{class u extends Error{constructor(E,f,R){super(R);Error.captureStackTrace(this,this.constructor),this.name=this.constructor.name,this.code=f,this.exitCode=E,this.nestedError=void 0}}class d extends u{constructor(E){super(1,"commander.invalidArgument",E);Error.captureStackTrace(this,this.constructor),this.name=this.constructor.name}}xE.CommanderError=u;xE.InvalidArgumentError=d});var N=X((OE)=>{var{InvalidArgumentError:uE}=K();class p{constructor(E,f){switch(this.description=f||"",this.variadic=!1,this.parseArg=void 0,this.defaultValue=void 0,this.defaultValueDescription=void 0,this.argChoices=void 0,E[0]){case"<":this.required=!0,this._name=E.slice(1,-1);break;case"[":this.required=!1,this._name=E.slice(1,-1);break;default:this.required=!0,this._name=E;break}if(this._name.length>3&&this._name.slice(-3)==="...")this.variadic=!0,this._name=this._name.slice(0,-3)}name(){return this._name}_concatValue(E,f){if(f===this.defaultValue||!Array.isArray(f))return[E];return f.concat(E)}default(E,f){return this.defaultValue=E,this.defaultValueDescription=f,this}argParser(E){return this.parseArg=E,this}choices(E){return this.argChoices=E.slice(),this.parseArg=(f,R)=>{if(!this.argChoices.includes(f))throw new uE(`Allowed choices are ${this.argChoices.join(", ")}.`);if(this.variadic)return this._concatValue(f,R);return f},this}argRequired(){return this.required=!0,this}argOptional(){return this.required=!1,this}}function hE(E){let f=E.name()+(E.variadic===!0?"...":"");return E.required?"<"+f+">":"["+f+"]"}OE.Argument=p;OE.humanReadableArgName=hE});var h=X((tE)=>{var{humanReadableArgName:sE}=N();class o{constructor(){this.helpWidth=void 0,this.sortSubcommands=!1,this.sortOptions=!1,this.showGlobalOptions=!1}visibleCommands(E){let f=E.commands.filter(($)=>!$._hidden),R=E._getHelpCommand();if(R&&!R._hidden)f.push(R);if(this.sortSubcommands)f.sort(($,S)=>{return $.name().localeCompare(S.name())});return f}compareOptions(E,f){let R=($)=>{return $.short?$.short.replace(/^-/,""):$.long.replace(/^--/,"")};return R(E).localeCompare(R(f))}visibleOptions(E){let f=E.options.filter(($)=>!$.hidden),R=E._getHelpOption();if(R&&!R.hidden){let $=R.short&&E._findOption(R.short),S=R.long&&E._findOption(R.long);if(!$&&!S)f.push(R);else if(R.long&&!S)f.push(E.createOption(R.long,R.description));else if(R.short&&!$)f.push(E.createOption(R.short,R.description))}if(this.sortOptions)f.sort(this.compareOptions);return f}visibleGlobalOptions(E){if(!this.showGlobalOptions)return[];let f=[];for(let R=E.parent;R;R=R.parent){let $=R.options.filter((S)=>!S.hidden);f.push(...$)}if(this.sortOptions)f.sort(this.compareOptions);return f}visibleArguments(E){if(E._argsDescription)E.registeredArguments.forEach((f)=>{f.description=f.description||E._argsDescription[f.name()]||""});if(E.registeredArguments.find((f)=>f.description))return E.registeredArguments;return[]}subcommandTerm(E){let f=E.registeredArguments.map((R)=>sE(R)).join(" ");return E._name+(E._aliases[0]?"|"+E._aliases[0]:"")+(E.options.length?" [options]":"")+(f?" "+f:"")}optionTerm(E){return E.flags}argumentTerm(E){return E.name()}longestSubcommandTermLength(E,f){return f.visibleCommands(E).reduce((R,$)=>{return Math.max(R,f.subcommandTerm($).length)},0)}longestOptionTermLength(E,f){return f.visibleOptions(E).reduce((R,$)=>{return Math.max(R,f.optionTerm($).length)},0)}longestGlobalOptionTermLength(E,f){return f.visibleGlobalOptions(E).reduce((R,$)=>{return Math.max(R,f.optionTerm($).length)},0)}longestArgumentTermLength(E,f){return f.visibleArguments(E).reduce((R,$)=>{return Math.max(R,f.argumentTerm($).length)},0)}commandUsage(E){let f=E._name;if(E._aliases[0])f=f+"|"+E._aliases[0];let R="";for(let $=E.parent;$;$=$.parent)R=$.name()+" "+R;return R+f+" "+E.usage()}commandDescription(E){return E.description()}subcommandDescription(E){return E.summary()||E.description()}optionDescription(E){let f=[];if(E.argChoices)f.push(`choices: ${E.argChoices.map((R)=>JSON.stringify(R)).join(", ")}`);if(E.defaultValue!==void 0){if(E.required||E.optional||E.isBoolean()&&typeof E.defaultValue==="boolean")f.push(`default: ${E.defaultValueDescription||JSON.stringify(E.defaultValue)}`)}if(E.presetArg!==void 0&&E.optional)f.push(`preset: ${JSON.stringify(E.presetArg)}`);if(E.envVar!==void 0)f.push(`env: ${E.envVar}`);if(f.length>0)return`${E.description} (${f.join(", ")})`;return E.description}argumentDescription(E){let f=[];if(E.argChoices)f.push(`choices: ${E.argChoices.map((R)=>JSON.stringify(R)).join(", ")}`);if(E.defaultValue!==void 0)f.push(`default: ${E.defaultValueDescription||JSON.stringify(E.defaultValue)}`);if(f.length>0){let R=`(${f.join(", ")})`;if(E.description)return`${E.description} ${R}`;return R}return E.description}formatHelp(E,f){let R=f.padWidth(E,f),$=f.helpWidth||80,S=2,T=2;function L(U,P){if(P){let C=`${U.padEnd(R+2)}${P}`;return f.wrap(C,$-2,R+2)}return U}function _(U){return U.join(`
|
|
3
|
+
`).replace(/^/gm," ".repeat(2))}let I=[`Usage: ${f.commandUsage(E)}`,""],w=f.commandDescription(E);if(w.length>0)I=I.concat([f.wrap(w,$,0),""]);let y=f.visibleArguments(E).map((U)=>{return L(f.argumentTerm(U),f.argumentDescription(U))});if(y.length>0)I=I.concat(["Arguments:",_(y),""]);let q=f.visibleOptions(E).map((U)=>{return L(f.optionTerm(U),f.optionDescription(U))});if(q.length>0)I=I.concat(["Options:",_(q),""]);if(this.showGlobalOptions){let U=f.visibleGlobalOptions(E).map((P)=>{return L(f.optionTerm(P),f.optionDescription(P))});if(U.length>0)I=I.concat(["Global Options:",_(U),""])}let Y=f.visibleCommands(E).map((U)=>{return L(f.subcommandTerm(U),f.subcommandDescription(U))});if(Y.length>0)I=I.concat(["Commands:",_(Y),""]);return I.join(`
|
|
4
|
+
`)}padWidth(E,f){return Math.max(f.longestOptionTermLength(E,f),f.longestGlobalOptionTermLength(E,f),f.longestSubcommandTermLength(E,f),f.longestArgumentTermLength(E,f))}wrap(E,f,R,$=40){let T=new RegExp(`[\\n][${" \\f\\t\\v - \uFEFF"}]+`);if(E.match(T))return E;let L=f-R;if(L<$)return E;let _=E.slice(0,R),I=E.slice(R).replace(`\r
|
|
5
|
+
`,`
|
|
6
|
+
`),w=" ".repeat(R),q=`\\s${""}`,Y=new RegExp(`
|
|
7
|
+
|.{1,${L-1}}([${q}]|$)|[^${q}]+?([${q}]|$)`,"g"),U=I.match(Y)||[];return _+U.map((P,C)=>{if(P===`
|
|
8
|
+
`)return"";return(C>0?w:"")+P.trimEnd()}).join(`
|
|
9
|
+
`)}}tE.Help=o});var O=X((lE)=>{var{InvalidArgumentError:gE}=K();class e{constructor(E,f){this.flags=E,this.description=f||"",this.required=E.includes("<"),this.optional=E.includes("["),this.variadic=/\w\.\.\.[>\]]$/.test(E),this.mandatory=!1;let R=iE(E);if(this.short=R.shortFlag,this.long=R.longFlag,this.negate=!1,this.long)this.negate=this.long.startsWith("--no-");this.defaultValue=void 0,this.defaultValueDescription=void 0,this.presetArg=void 0,this.envVar=void 0,this.parseArg=void 0,this.hidden=!1,this.argChoices=void 0,this.conflictsWith=[],this.implied=void 0}default(E,f){return this.defaultValue=E,this.defaultValueDescription=f,this}preset(E){return this.presetArg=E,this}conflicts(E){return this.conflictsWith=this.conflictsWith.concat(E),this}implies(E){let f=E;if(typeof E==="string")f={[E]:!0};return this.implied=Object.assign(this.implied||{},f),this}env(E){return this.envVar=E,this}argParser(E){return this.parseArg=E,this}makeOptionMandatory(E=!0){return this.mandatory=!!E,this}hideHelp(E=!0){return this.hidden=!!E,this}_concatValue(E,f){if(f===this.defaultValue||!Array.isArray(f))return[E];return f.concat(E)}choices(E){return this.argChoices=E.slice(),this.parseArg=(f,R)=>{if(!this.argChoices.includes(f))throw new gE(`Allowed choices are ${this.argChoices.join(", ")}.`);if(this.variadic)return this._concatValue(f,R);return f},this}name(){if(this.long)return this.long.replace(/^--/,"");return this.short.replace(/^-/,"")}attributeName(){return nE(this.name().replace(/^no-/,""))}is(E){return this.short===E||this.long===E}isBoolean(){return!this.required&&!this.optional&&!this.negate}}class a{constructor(E){this.positiveOptions=new Map,this.negativeOptions=new Map,this.dualOptions=new Set,E.forEach((f)=>{if(f.negate)this.negativeOptions.set(f.attributeName(),f);else this.positiveOptions.set(f.attributeName(),f)}),this.negativeOptions.forEach((f,R)=>{if(this.positiveOptions.has(R))this.dualOptions.add(R)})}valueFromOption(E,f){let R=f.attributeName();if(!this.dualOptions.has(R))return!0;let $=this.negativeOptions.get(R).presetArg,S=$!==void 0?$:!1;return f.negate===(S===E)}}function nE(E){return E.split("-").reduce((f,R)=>{return f+R[0].toUpperCase()+R.slice(1)})}function iE(E){let f,R,$=E.split(/[ |,]+/);if($.length>1&&!/^[[<]/.test($[1]))f=$.shift();if(R=$.shift(),!f&&/^-[^-]$/.test(R))f=R,R=void 0;return{shortFlag:f,longFlag:R}}lE.Option=e;lE.DualOptions=a});var EE=X((eE)=>{function pE(E,f){if(Math.abs(E.length-f.length)>3)return Math.max(E.length,f.length);let R=[];for(let $=0;$<=E.length;$++)R[$]=[$];for(let $=0;$<=f.length;$++)R[0][$]=$;for(let $=1;$<=f.length;$++)for(let S=1;S<=E.length;S++){let T=1;if(E[S-1]===f[$-1])T=0;else T=1;if(R[S][$]=Math.min(R[S-1][$]+1,R[S][$-1]+1,R[S-1][$-1]+T),S>1&&$>1&&E[S-1]===f[$-2]&&E[S-2]===f[$-1])R[S][$]=Math.min(R[S][$],R[S-2][$-2]+1)}return R[E.length][f.length]}function oE(E,f){if(!f||f.length===0)return"";f=Array.from(new Set(f));let R=E.startsWith("--");if(R)E=E.slice(2),f=f.map((L)=>L.slice(2));let $=[],S=3,T=0.4;if(f.forEach((L)=>{if(L.length<=1)return;let _=pE(E,L),I=Math.max(E.length,L.length);if((I-_)/I>T){if(_<S)S=_,$=[L];else if(_===S)$.push(L)}}),$.sort((L,_)=>L.localeCompare(_)),R)$=$.map((L)=>`--${L}`);if($.length>1)return`
|
|
10
|
+
(Did you mean one of ${$.join(", ")}?)`;if($.length===1)return`
|
|
11
|
+
(Did you mean ${$[0]}?)`;return""}eE.suggestSimilar=oE});var SE=X((Tf)=>{var Ef=Q("node:events").EventEmitter,v=Q("node:child_process"),M=Q("node:path"),r=Q("node:fs"),J=Q("node:process"),{Argument:ff,humanReadableArgName:Rf}=N(),{CommanderError:s}=K(),{Help:$f}=h(),{Option:fE,DualOptions:Sf}=O(),{suggestSimilar:RE}=EE();class t extends Ef{constructor(E){super();this.commands=[],this.options=[],this.parent=null,this._allowUnknownOption=!1,this._allowExcessArguments=!0,this.registeredArguments=[],this._args=this.registeredArguments,this.args=[],this.rawArgs=[],this.processedArgs=[],this._scriptPath=null,this._name=E||"",this._optionValues={},this._optionValueSources={},this._storeOptionsAsProperties=!1,this._actionHandler=null,this._executableHandler=!1,this._executableFile=null,this._executableDir=null,this._defaultCommandName=null,this._exitCallback=null,this._aliases=[],this._combineFlagAndOptionalValue=!0,this._description="",this._summary="",this._argsDescription=void 0,this._enablePositionalOptions=!1,this._passThroughOptions=!1,this._lifeCycleHooks={},this._showHelpAfterError=!1,this._showSuggestionAfterError=!0,this._outputConfiguration={writeOut:(f)=>J.stdout.write(f),writeErr:(f)=>J.stderr.write(f),getOutHelpWidth:()=>J.stdout.isTTY?J.stdout.columns:void 0,getErrHelpWidth:()=>J.stderr.isTTY?J.stderr.columns:void 0,outputError:(f,R)=>R(f)},this._hidden=!1,this._helpOption=void 0,this._addImplicitHelpCommand=void 0,this._helpCommand=void 0,this._helpConfiguration={}}copyInheritedSettings(E){return this._outputConfiguration=E._outputConfiguration,this._helpOption=E._helpOption,this._helpCommand=E._helpCommand,this._helpConfiguration=E._helpConfiguration,this._exitCallback=E._exitCallback,this._storeOptionsAsProperties=E._storeOptionsAsProperties,this._combineFlagAndOptionalValue=E._combineFlagAndOptionalValue,this._allowExcessArguments=E._allowExcessArguments,this._enablePositionalOptions=E._enablePositionalOptions,this._showHelpAfterError=E._showHelpAfterError,this._showSuggestionAfterError=E._showSuggestionAfterError,this}_getCommandAndAncestors(){let E=[];for(let f=this;f;f=f.parent)E.push(f);return E}command(E,f,R){let $=f,S=R;if(typeof $==="object"&&$!==null)S=$,$=null;S=S||{};let[,T,L]=E.match(/([^ ]+) *(.*)/),_=this.createCommand(T);if($)_.description($),_._executableHandler=!0;if(S.isDefault)this._defaultCommandName=_._name;if(_._hidden=!!(S.noHelp||S.hidden),_._executableFile=S.executableFile||null,L)_.arguments(L);if(this._registerCommand(_),_.parent=this,_.copyInheritedSettings(this),$)return this;return _}createCommand(E){return new t(E)}createHelp(){return Object.assign(new $f,this.configureHelp())}configureHelp(E){if(E===void 0)return this._helpConfiguration;return this._helpConfiguration=E,this}configureOutput(E){if(E===void 0)return this._outputConfiguration;return Object.assign(this._outputConfiguration,E),this}showHelpAfterError(E=!0){if(typeof E!=="string")E=!!E;return this._showHelpAfterError=E,this}showSuggestionAfterError(E=!0){return this._showSuggestionAfterError=!!E,this}addCommand(E,f){if(!E._name)throw new Error(`Command passed to .addCommand() must have a name
|
|
12
|
+
- specify the name in Command constructor or using .name()`);if(f=f||{},f.isDefault)this._defaultCommandName=E._name;if(f.noHelp||f.hidden)E._hidden=!0;return this._registerCommand(E),E.parent=this,E._checkForBrokenPassThrough(),this}createArgument(E,f){return new ff(E,f)}argument(E,f,R,$){let S=this.createArgument(E,f);if(typeof R==="function")S.default($).argParser(R);else S.default(R);return this.addArgument(S),this}arguments(E){return E.trim().split(/ +/).forEach((f)=>{this.argument(f)}),this}addArgument(E){let f=this.registeredArguments.slice(-1)[0];if(f&&f.variadic)throw new Error(`only the last argument can be variadic '${f.name()}'`);if(E.required&&E.defaultValue!==void 0&&E.parseArg===void 0)throw new Error(`a default value for a required argument is never used: '${E.name()}'`);return this.registeredArguments.push(E),this}helpCommand(E,f){if(typeof E==="boolean")return this._addImplicitHelpCommand=E,this;E=E??"help [command]";let[,R,$]=E.match(/([^ ]+) *(.*)/),S=f??"display help for command",T=this.createCommand(R);if(T.helpOption(!1),$)T.arguments($);if(S)T.description(S);return this._addImplicitHelpCommand=!0,this._helpCommand=T,this}addHelpCommand(E,f){if(typeof E!=="object")return this.helpCommand(E,f),this;return this._addImplicitHelpCommand=!0,this._helpCommand=E,this}_getHelpCommand(){if(this._addImplicitHelpCommand??(this.commands.length&&!this._actionHandler&&!this._findCommand("help"))){if(this._helpCommand===void 0)this.helpCommand(void 0,void 0);return this._helpCommand}return null}hook(E,f){let R=["preSubcommand","preAction","postAction"];if(!R.includes(E))throw new Error(`Unexpected value for event passed to hook : '${E}'.
|
|
13
|
+
Expecting one of '${R.join("', '")}'`);if(this._lifeCycleHooks[E])this._lifeCycleHooks[E].push(f);else this._lifeCycleHooks[E]=[f];return this}exitOverride(E){if(E)this._exitCallback=E;else this._exitCallback=(f)=>{if(f.code!=="commander.executeSubCommandAsync")throw f};return this}_exit(E,f,R){if(this._exitCallback)this._exitCallback(new s(E,f,R));J.exit(E)}action(E){let f=(R)=>{let $=this.registeredArguments.length,S=R.slice(0,$);if(this._storeOptionsAsProperties)S[$]=this;else S[$]=this.opts();return S.push(this),E.apply(this,S)};return this._actionHandler=f,this}createOption(E,f){return new fE(E,f)}_callParseArg(E,f,R,$){try{return E.parseArg(f,R)}catch(S){if(S.code==="commander.invalidArgument"){let T=`${$} ${S.message}`;this.error(T,{exitCode:S.exitCode,code:S.code})}throw S}}_registerOption(E){let f=E.short&&this._findOption(E.short)||E.long&&this._findOption(E.long);if(f){let R=E.long&&this._findOption(E.long)?E.long:E.short;throw new Error(`Cannot add option '${E.flags}'${this._name&&` to command '${this._name}'`} due to conflicting flag '${R}'
|
|
14
|
+
- already used by option '${f.flags}'`)}this.options.push(E)}_registerCommand(E){let f=($)=>{return[$.name()].concat($.aliases())},R=f(E).find(($)=>this._findCommand($));if(R){let $=f(this._findCommand(R)).join("|"),S=f(E).join("|");throw new Error(`cannot add command '${S}' as already have command '${$}'`)}this.commands.push(E)}addOption(E){this._registerOption(E);let f=E.name(),R=E.attributeName();if(E.negate){let S=E.long.replace(/^--no-/,"--");if(!this._findOption(S))this.setOptionValueWithSource(R,E.defaultValue===void 0?!0:E.defaultValue,"default")}else if(E.defaultValue!==void 0)this.setOptionValueWithSource(R,E.defaultValue,"default");let $=(S,T,L)=>{if(S==null&&E.presetArg!==void 0)S=E.presetArg;let _=this.getOptionValue(R);if(S!==null&&E.parseArg)S=this._callParseArg(E,S,_,T);else if(S!==null&&E.variadic)S=E._concatValue(S,_);if(S==null)if(E.negate)S=!1;else if(E.isBoolean()||E.optional)S=!0;else S="";this.setOptionValueWithSource(R,S,L)};if(this.on("option:"+f,(S)=>{let T=`error: option '${E.flags}' argument '${S}' is invalid.`;$(S,T,"cli")}),E.envVar)this.on("optionEnv:"+f,(S)=>{let T=`error: option '${E.flags}' value '${S}' from env '${E.envVar}' is invalid.`;$(S,T,"env")});return this}_optionEx(E,f,R,$,S){if(typeof f==="object"&&f instanceof fE)throw new Error("To add an Option object use addOption() instead of option() or requiredOption()");let T=this.createOption(f,R);if(T.makeOptionMandatory(!!E.mandatory),typeof $==="function")T.default(S).argParser($);else if($ instanceof RegExp){let L=$;$=(_,I)=>{let w=L.exec(_);return w?w[0]:I},T.default(S).argParser($)}else T.default($);return this.addOption(T)}option(E,f,R,$){return this._optionEx({},E,f,R,$)}requiredOption(E,f,R,$){return this._optionEx({mandatory:!0},E,f,R,$)}combineFlagAndOptionalValue(E=!0){return this._combineFlagAndOptionalValue=!!E,this}allowUnknownOption(E=!0){return this._allowUnknownOption=!!E,this}allowExcessArguments(E=!0){return this._allowExcessArguments=!!E,this}enablePositionalOptions(E=!0){return this._enablePositionalOptions=!!E,this}passThroughOptions(E=!0){return this._passThroughOptions=!!E,this._checkForBrokenPassThrough(),this}_checkForBrokenPassThrough(){if(this.parent&&this._passThroughOptions&&!this.parent._enablePositionalOptions)throw new Error(`passThroughOptions cannot be used for '${this._name}' without turning on enablePositionalOptions for parent command(s)`)}storeOptionsAsProperties(E=!0){if(this.options.length)throw new Error("call .storeOptionsAsProperties() before adding options");if(Object.keys(this._optionValues).length)throw new Error("call .storeOptionsAsProperties() before setting option values");return this._storeOptionsAsProperties=!!E,this}getOptionValue(E){if(this._storeOptionsAsProperties)return this[E];return this._optionValues[E]}setOptionValue(E,f){return this.setOptionValueWithSource(E,f,void 0)}setOptionValueWithSource(E,f,R){if(this._storeOptionsAsProperties)this[E]=f;else this._optionValues[E]=f;return this._optionValueSources[E]=R,this}getOptionValueSource(E){return this._optionValueSources[E]}getOptionValueSourceWithGlobals(E){let f;return this._getCommandAndAncestors().forEach((R)=>{if(R.getOptionValueSource(E)!==void 0)f=R.getOptionValueSource(E)}),f}_prepareUserArgs(E,f){if(E!==void 0&&!Array.isArray(E))throw new Error("first parameter to parse must be array or undefined");if(f=f||{},E===void 0&&f.from===void 0){if(J.versions?.electron)f.from="electron";let $=J.execArgv??[];if($.includes("-e")||$.includes("--eval")||$.includes("-p")||$.includes("--print"))f.from="eval"}if(E===void 0)E=J.argv;this.rawArgs=E.slice();let R;switch(f.from){case void 0:case"node":this._scriptPath=E[1],R=E.slice(2);break;case"electron":if(J.defaultApp)this._scriptPath=E[1],R=E.slice(2);else R=E.slice(1);break;case"user":R=E.slice(0);break;case"eval":R=E.slice(1);break;default:throw new Error(`unexpected parse option { from: '${f.from}' }`)}if(!this._name&&this._scriptPath)this.nameFromFilename(this._scriptPath);return this._name=this._name||"program",R}parse(E,f){let R=this._prepareUserArgs(E,f);return this._parseCommand([],R),this}async parseAsync(E,f){let R=this._prepareUserArgs(E,f);return await this._parseCommand([],R),this}_executeSubCommand(E,f){f=f.slice();let R=!1,$=[".js",".ts",".tsx",".mjs",".cjs"];function S(w,y){let q=M.resolve(w,y);if(r.existsSync(q))return q;if($.includes(M.extname(y)))return;let Y=$.find((U)=>r.existsSync(`${q}${U}`));if(Y)return`${q}${Y}`;return}this._checkForMissingMandatoryOptions(),this._checkForConflictingOptions();let T=E._executableFile||`${this._name}-${E._name}`,L=this._executableDir||"";if(this._scriptPath){let w;try{w=r.realpathSync(this._scriptPath)}catch(y){w=this._scriptPath}L=M.resolve(M.dirname(w),L)}if(L){let w=S(L,T);if(!w&&!E._executableFile&&this._scriptPath){let y=M.basename(this._scriptPath,M.extname(this._scriptPath));if(y!==this._name)w=S(L,`${y}-${E._name}`)}T=w||T}R=$.includes(M.extname(T));let _;if(J.platform!=="win32")if(R)f.unshift(T),f=$E(J.execArgv).concat(f),_=v.spawn(J.argv[0],f,{stdio:"inherit"});else _=v.spawn(T,f,{stdio:"inherit"});else f.unshift(T),f=$E(J.execArgv).concat(f),_=v.spawn(J.execPath,f,{stdio:"inherit"});if(!_.killed)["SIGUSR1","SIGUSR2","SIGTERM","SIGINT","SIGHUP"].forEach((y)=>{J.on(y,()=>{if(_.killed===!1&&_.exitCode===null)_.kill(y)})});let I=this._exitCallback;_.on("close",(w)=>{if(w=w??1,!I)J.exit(w);else I(new s(w,"commander.executeSubCommandAsync","(close)"))}),_.on("error",(w)=>{if(w.code==="ENOENT"){let y=L?`searched for local subcommand relative to directory '${L}'`:"no directory for search for local subcommand, use .executableDir() to supply a custom directory",q=`'${T}' does not exist
|
|
15
|
+
- if '${E._name}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
|
|
16
|
+
- if the default executable name is not suitable, use the executableFile option to supply a custom name or path
|
|
17
|
+
- ${y}`;throw new Error(q)}else if(w.code==="EACCES")throw new Error(`'${T}' not executable`);if(!I)J.exit(1);else{let y=new s(1,"commander.executeSubCommandAsync","(error)");y.nestedError=w,I(y)}}),this.runningCommand=_}_dispatchSubcommand(E,f,R){let $=this._findCommand(E);if(!$)this.help({error:!0});let S;return S=this._chainOrCallSubCommandHook(S,$,"preSubcommand"),S=this._chainOrCall(S,()=>{if($._executableHandler)this._executeSubCommand($,f.concat(R));else return $._parseCommand(f,R)}),S}_dispatchHelpCommand(E){if(!E)this.help();let f=this._findCommand(E);if(f&&!f._executableHandler)f.help();return this._dispatchSubcommand(E,[],[this._getHelpOption()?.long??this._getHelpOption()?.short??"--help"])}_checkNumberOfArguments(){if(this.registeredArguments.forEach((E,f)=>{if(E.required&&this.args[f]==null)this.missingArgument(E.name())}),this.registeredArguments.length>0&&this.registeredArguments[this.registeredArguments.length-1].variadic)return;if(this.args.length>this.registeredArguments.length)this._excessArguments(this.args)}_processArguments(){let E=(R,$,S)=>{let T=$;if($!==null&&R.parseArg){let L=`error: command-argument value '${$}' is invalid for argument '${R.name()}'.`;T=this._callParseArg(R,$,S,L)}return T};this._checkNumberOfArguments();let f=[];this.registeredArguments.forEach((R,$)=>{let S=R.defaultValue;if(R.variadic){if($<this.args.length){if(S=this.args.slice($),R.parseArg)S=S.reduce((T,L)=>{return E(R,L,T)},R.defaultValue)}else if(S===void 0)S=[]}else if($<this.args.length){if(S=this.args[$],R.parseArg)S=E(R,S,R.defaultValue)}f[$]=S}),this.processedArgs=f}_chainOrCall(E,f){if(E&&E.then&&typeof E.then==="function")return E.then(()=>f());return f()}_chainOrCallHooks(E,f){let R=E,$=[];if(this._getCommandAndAncestors().reverse().filter((S)=>S._lifeCycleHooks[f]!==void 0).forEach((S)=>{S._lifeCycleHooks[f].forEach((T)=>{$.push({hookedCommand:S,callback:T})})}),f==="postAction")$.reverse();return $.forEach((S)=>{R=this._chainOrCall(R,()=>{return S.callback(S.hookedCommand,this)})}),R}_chainOrCallSubCommandHook(E,f,R){let $=E;if(this._lifeCycleHooks[R]!==void 0)this._lifeCycleHooks[R].forEach((S)=>{$=this._chainOrCall($,()=>{return S(this,f)})});return $}_parseCommand(E,f){let R=this.parseOptions(f);if(this._parseOptionsEnv(),this._parseOptionsImplied(),E=E.concat(R.operands),f=R.unknown,this.args=E.concat(f),E&&this._findCommand(E[0]))return this._dispatchSubcommand(E[0],E.slice(1),f);if(this._getHelpCommand()&&E[0]===this._getHelpCommand().name())return this._dispatchHelpCommand(E[1]);if(this._defaultCommandName)return this._outputHelpIfRequested(f),this._dispatchSubcommand(this._defaultCommandName,E,f);if(this.commands.length&&this.args.length===0&&!this._actionHandler&&!this._defaultCommandName)this.help({error:!0});this._outputHelpIfRequested(R.unknown),this._checkForMissingMandatoryOptions(),this._checkForConflictingOptions();let $=()=>{if(R.unknown.length>0)this.unknownOption(R.unknown[0])},S=`command:${this.name()}`;if(this._actionHandler){$(),this._processArguments();let T;if(T=this._chainOrCallHooks(T,"preAction"),T=this._chainOrCall(T,()=>this._actionHandler(this.processedArgs)),this.parent)T=this._chainOrCall(T,()=>{this.parent.emit(S,E,f)});return T=this._chainOrCallHooks(T,"postAction"),T}if(this.parent&&this.parent.listenerCount(S))$(),this._processArguments(),this.parent.emit(S,E,f);else if(E.length){if(this._findCommand("*"))return this._dispatchSubcommand("*",E,f);if(this.listenerCount("command:*"))this.emit("command:*",E,f);else if(this.commands.length)this.unknownCommand();else $(),this._processArguments()}else if(this.commands.length)$(),this.help({error:!0});else $(),this._processArguments()}_findCommand(E){if(!E)return;return this.commands.find((f)=>f._name===E||f._aliases.includes(E))}_findOption(E){return this.options.find((f)=>f.is(E))}_checkForMissingMandatoryOptions(){this._getCommandAndAncestors().forEach((E)=>{E.options.forEach((f)=>{if(f.mandatory&&E.getOptionValue(f.attributeName())===void 0)E.missingMandatoryOptionValue(f)})})}_checkForConflictingLocalOptions(){let E=this.options.filter((R)=>{let $=R.attributeName();if(this.getOptionValue($)===void 0)return!1;return this.getOptionValueSource($)!=="default"});E.filter((R)=>R.conflictsWith.length>0).forEach((R)=>{let $=E.find((S)=>R.conflictsWith.includes(S.attributeName()));if($)this._conflictingOption(R,$)})}_checkForConflictingOptions(){this._getCommandAndAncestors().forEach((E)=>{E._checkForConflictingLocalOptions()})}parseOptions(E){let f=[],R=[],$=f,S=E.slice();function T(_){return _.length>1&&_[0]==="-"}let L=null;while(S.length){let _=S.shift();if(_==="--"){if($===R)$.push(_);$.push(...S);break}if(L&&!T(_)){this.emit(`option:${L.name()}`,_);continue}if(L=null,T(_)){let I=this._findOption(_);if(I){if(I.required){let w=S.shift();if(w===void 0)this.optionMissingArgument(I);this.emit(`option:${I.name()}`,w)}else if(I.optional){let w=null;if(S.length>0&&!T(S[0]))w=S.shift();this.emit(`option:${I.name()}`,w)}else this.emit(`option:${I.name()}`);L=I.variadic?I:null;continue}}if(_.length>2&&_[0]==="-"&&_[1]!=="-"){let I=this._findOption(`-${_[1]}`);if(I){if(I.required||I.optional&&this._combineFlagAndOptionalValue)this.emit(`option:${I.name()}`,_.slice(2));else this.emit(`option:${I.name()}`),S.unshift(`-${_.slice(2)}`);continue}}if(/^--[^=]+=/.test(_)){let I=_.indexOf("="),w=this._findOption(_.slice(0,I));if(w&&(w.required||w.optional)){this.emit(`option:${w.name()}`,_.slice(I+1));continue}}if(T(_))$=R;if((this._enablePositionalOptions||this._passThroughOptions)&&f.length===0&&R.length===0){if(this._findCommand(_)){if(f.push(_),S.length>0)R.push(...S);break}else if(this._getHelpCommand()&&_===this._getHelpCommand().name()){if(f.push(_),S.length>0)f.push(...S);break}else if(this._defaultCommandName){if(R.push(_),S.length>0)R.push(...S);break}}if(this._passThroughOptions){if($.push(_),S.length>0)$.push(...S);break}$.push(_)}return{operands:f,unknown:R}}opts(){if(this._storeOptionsAsProperties){let E={},f=this.options.length;for(let R=0;R<f;R++){let $=this.options[R].attributeName();E[$]=$===this._versionOptionName?this._version:this[$]}return E}return this._optionValues}optsWithGlobals(){return this._getCommandAndAncestors().reduce((E,f)=>Object.assign(E,f.opts()),{})}error(E,f){if(this._outputConfiguration.outputError(`${E}
|
|
18
|
+
`,this._outputConfiguration.writeErr),typeof this._showHelpAfterError==="string")this._outputConfiguration.writeErr(`${this._showHelpAfterError}
|
|
19
|
+
`);else if(this._showHelpAfterError)this._outputConfiguration.writeErr(`
|
|
20
|
+
`),this.outputHelp({error:!0});let R=f||{},$=R.exitCode||1,S=R.code||"commander.error";this._exit($,S,E)}_parseOptionsEnv(){this.options.forEach((E)=>{if(E.envVar&&E.envVar in J.env){let f=E.attributeName();if(this.getOptionValue(f)===void 0||["default","config","env"].includes(this.getOptionValueSource(f)))if(E.required||E.optional)this.emit(`optionEnv:${E.name()}`,J.env[E.envVar]);else this.emit(`optionEnv:${E.name()}`)}})}_parseOptionsImplied(){let E=new Sf(this.options),f=(R)=>{return this.getOptionValue(R)!==void 0&&!["default","implied"].includes(this.getOptionValueSource(R))};this.options.filter((R)=>R.implied!==void 0&&f(R.attributeName())&&E.valueFromOption(this.getOptionValue(R.attributeName()),R)).forEach((R)=>{Object.keys(R.implied).filter(($)=>!f($)).forEach(($)=>{this.setOptionValueWithSource($,R.implied[$],"implied")})})}missingArgument(E){let f=`error: missing required argument '${E}'`;this.error(f,{code:"commander.missingArgument"})}optionMissingArgument(E){let f=`error: option '${E.flags}' argument missing`;this.error(f,{code:"commander.optionMissingArgument"})}missingMandatoryOptionValue(E){let f=`error: required option '${E.flags}' not specified`;this.error(f,{code:"commander.missingMandatoryOptionValue"})}_conflictingOption(E,f){let R=(T)=>{let L=T.attributeName(),_=this.getOptionValue(L),I=this.options.find((y)=>y.negate&&L===y.attributeName()),w=this.options.find((y)=>!y.negate&&L===y.attributeName());if(I&&(I.presetArg===void 0&&_===!1||I.presetArg!==void 0&&_===I.presetArg))return I;return w||T},$=(T)=>{let L=R(T),_=L.attributeName();if(this.getOptionValueSource(_)==="env")return`environment variable '${L.envVar}'`;return`option '${L.flags}'`},S=`error: ${$(E)} cannot be used with ${$(f)}`;this.error(S,{code:"commander.conflictingOption"})}unknownOption(E){if(this._allowUnknownOption)return;let f="";if(E.startsWith("--")&&this._showSuggestionAfterError){let $=[],S=this;do{let T=S.createHelp().visibleOptions(S).filter((L)=>L.long).map((L)=>L.long);$=$.concat(T),S=S.parent}while(S&&!S._enablePositionalOptions);f=RE(E,$)}let R=`error: unknown option '${E}'${f}`;this.error(R,{code:"commander.unknownOption"})}_excessArguments(E){if(this._allowExcessArguments)return;let f=this.registeredArguments.length,R=f===1?"":"s",S=`error: too many arguments${this.parent?` for '${this.name()}'`:""}. Expected ${f} argument${R} but got ${E.length}.`;this.error(S,{code:"commander.excessArguments"})}unknownCommand(){let E=this.args[0],f="";if(this._showSuggestionAfterError){let $=[];this.createHelp().visibleCommands(this).forEach((S)=>{if($.push(S.name()),S.alias())$.push(S.alias())}),f=RE(E,$)}let R=`error: unknown command '${E}'${f}`;this.error(R,{code:"commander.unknownCommand"})}version(E,f,R){if(E===void 0)return this._version;this._version=E,f=f||"-V, --version",R=R||"output the version number";let $=this.createOption(f,R);return this._versionOptionName=$.attributeName(),this._registerOption($),this.on("option:"+$.name(),()=>{this._outputConfiguration.writeOut(`${E}
|
|
21
|
+
`),this._exit(0,"commander.version",E)}),this}description(E,f){if(E===void 0&&f===void 0)return this._description;if(this._description=E,f)this._argsDescription=f;return this}summary(E){if(E===void 0)return this._summary;return this._summary=E,this}alias(E){if(E===void 0)return this._aliases[0];let f=this;if(this.commands.length!==0&&this.commands[this.commands.length-1]._executableHandler)f=this.commands[this.commands.length-1];if(E===f._name)throw new Error("Command alias can't be the same as its name");let R=this.parent?._findCommand(E);if(R){let $=[R.name()].concat(R.aliases()).join("|");throw new Error(`cannot add alias '${E}' to command '${this.name()}' as already have command '${$}'`)}return f._aliases.push(E),this}aliases(E){if(E===void 0)return this._aliases;return E.forEach((f)=>this.alias(f)),this}usage(E){if(E===void 0){if(this._usage)return this._usage;let f=this.registeredArguments.map((R)=>{return Rf(R)});return[].concat(this.options.length||this._helpOption!==null?"[options]":[],this.commands.length?"[command]":[],this.registeredArguments.length?f:[]).join(" ")}return this._usage=E,this}name(E){if(E===void 0)return this._name;return this._name=E,this}nameFromFilename(E){return this._name=M.basename(E,M.extname(E)),this}executableDir(E){if(E===void 0)return this._executableDir;return this._executableDir=E,this}helpInformation(E){let f=this.createHelp();if(f.helpWidth===void 0)f.helpWidth=E&&E.error?this._outputConfiguration.getErrHelpWidth():this._outputConfiguration.getOutHelpWidth();return f.formatHelp(this,f)}_getHelpContext(E){E=E||{};let f={error:!!E.error},R;if(f.error)R=($)=>this._outputConfiguration.writeErr($);else R=($)=>this._outputConfiguration.writeOut($);return f.write=E.write||R,f.command=this,f}outputHelp(E){let f;if(typeof E==="function")f=E,E=void 0;let R=this._getHelpContext(E);this._getCommandAndAncestors().reverse().forEach((S)=>S.emit("beforeAllHelp",R)),this.emit("beforeHelp",R);let $=this.helpInformation(R);if(f){if($=f($),typeof $!=="string"&&!Buffer.isBuffer($))throw new Error("outputHelp callback must return a string or a Buffer")}if(R.write($),this._getHelpOption()?.long)this.emit(this._getHelpOption().long);this.emit("afterHelp",R),this._getCommandAndAncestors().forEach((S)=>S.emit("afterAllHelp",R))}helpOption(E,f){if(typeof E==="boolean"){if(E)this._helpOption=this._helpOption??void 0;else this._helpOption=null;return this}return E=E??"-h, --help",f=f??"display help for command",this._helpOption=this.createOption(E,f),this}_getHelpOption(){if(this._helpOption===void 0)this.helpOption(void 0,void 0);return this._helpOption}addHelpOption(E){return this._helpOption=E,this}help(E){this.outputHelp(E);let f=J.exitCode||0;if(f===0&&E&&typeof E!=="function"&&E.error)f=1;this._exit(f,"commander.help","(outputHelp)")}addHelpText(E,f){let R=["beforeAll","before","after","afterAll"];if(!R.includes(E))throw new Error(`Unexpected value for position to addHelpText.
|
|
22
|
+
Expecting one of '${R.join("', '")}'`);let $=`${E}Help`;return this.on($,(S)=>{let T;if(typeof f==="function")T=f({error:S.error,command:S.command});else T=f;if(T)S.write(`${T}
|
|
23
|
+
`)}),this}_outputHelpIfRequested(E){let f=this._getHelpOption();if(f&&E.find(($)=>f.is($)))this.outputHelp(),this._exit(0,"commander.helpDisplayed","(outputHelp)")}}function $E(E){return E.map((f)=>{if(!f.startsWith("--inspect"))return f;let R,$="127.0.0.1",S="9229",T;if((T=f.match(/^(--inspect(-brk)?)$/))!==null)R=T[1];else if((T=f.match(/^(--inspect(-brk|-port)?)=([^:]+)$/))!==null)if(R=T[1],/^\d+$/.test(T[3]))S=T[3];else $=T[3];else if((T=f.match(/^(--inspect(-brk|-port)?)=([^:]+):(\d+)$/))!==null)R=T[1],$=T[3],S=T[4];if(R&&S!=="0")return`${R}=${$}:${parseInt(S)+1}`;return f})}Tf.Command=t});var IE=X((wf)=>{var{Argument:TE}=N(),{Command:c}=SE(),{CommanderError:Lf,InvalidArgumentError:_E}=K(),{Help:If}=h(),{Option:LE}=O();wf.program=new c;wf.createCommand=(E)=>new c(E);wf.createOption=(E,f)=>new LE(E,f);wf.createArgument=(E,f)=>new TE(E,f);wf.Command=c;wf.Option=LE;wf.Argument=TE;wf.Help=If;wf.CommanderError=Lf;wf.InvalidArgumentError=_E;wf.InvalidOptionArgumentError=_E});var wE=VE(IE(),1),{program:P0,createCommand:G0,createArgument:Y0,createOption:X0,CommanderError:z0,InvalidArgumentError:Z0,InvalidOptionArgumentError:j0,Command:G,Argument:B0,Option:Q0,Help:K0}=wE.default;import{readFile as sf}from"node:fs/promises";import{resolve as tf}from"node:path";var yE={name:"@shotstack/cli",version:"0.1.0",description:"Command-line interface for the Shotstack video rendering API.",license:"Apache-2.0",homepage:"https://github.com/shotstack/shotstack-cli",repository:{type:"git",url:"git+https://github.com/shotstack/shotstack-cli.git"},bugs:{url:"https://github.com/shotstack/shotstack-cli/issues"},type:"module",bin:{shotstack:"./dist/shotstack.js"},files:["dist/shotstack.js","SKILL.md","references"],scripts:{start:"bun run src/index.ts",build:"bun build src/index.ts --target=node --minify --outfile=dist/shotstack.js --banner='#!/usr/bin/env node' && chmod +x dist/shotstack.js",prepack:"bun run build",typecheck:"tsc --noEmit",test:"bun test"},dependencies:{commander:"^12.1.0"},devDependencies:{"@types/bun":"latest",typescript:"^5.6.0"}};var H=yE.version,JE=`shotstack-cli/${H} (${process.platform}-${process.arch})`;class z extends Error{status;body;constructor(E,f,R){super(R);this.status=E;this.body=f;this.name="ApiError"}get isTransient(){return this.status>=500||this.status===429}}function W(E){let f=E.env.baseUrl.replace(/\/$/,"");async function R($,S,T){let L=await fetch(`${f}${S}`,{method:$,headers:{"x-api-key":E.apiKey,"x-shotstack-origin":"cli","x-shotstack-environment":E.env.name,"user-agent":JE,...T!==void 0?{"content-type":"application/json"}:{}},body:T!==void 0?JSON.stringify(T):void 0}),_=await L.text(),I=_?Bf(_):void 0;if(!L.ok)throw new z(L.status,I??_,`${$} ${S} → ${L.status} ${L.statusText}`);return I}return{post:($,S)=>R("POST",$,S),get:($)=>R("GET",$)}}function Bf(E){try{return JSON.parse(E)}catch{return E}}class Z extends Error{constructor(){super("SHOTSTACK_API_KEY is not set. Get a key at https://shotstack.io.");this.name="MissingApiKeyError"}}function D(){let E=process.env.SHOTSTACK_API_KEY?.trim();if(!E)throw new Z;return E}var UE={dev:"https://api.shotstack.io/edit/dev",stage:"https://api.shotstack.io/edit/stage",v1:"https://api.shotstack.io/edit/v1"},j=Object.keys(UE);class B extends Error{constructor(E){super(`Invalid environment "${E}". Use one of: ${j.join(", ")}.`);this.name="InvalidEnvError"}}function F(E){let f=process.env.SHOTSTACK_API_BASE_URL?.trim();if(f)return{name:"custom",baseUrl:f};let R=E??process.env.SHOTSTACK_ENV?.trim()??"v1";if(!Qf(R))throw new B(R);return{name:R,baseUrl:UE[R]}}function Qf(E){return j.includes(E)}function k(E){if(E==="json")return"json";return"text"}function V(E,f,R){if(E==="json")console.log(JSON.stringify(f));else console.log(R)}import{mkdir as Kf,appendFile as Hf,stat as GE,readFile as Nf,writeFile as YE}from"node:fs/promises";import{join as g,dirname as Wf}from"node:path";import{homedir as qE}from"node:os";var Df=1e6,Ff=500000,ME=2000;function A(){if(process.platform==="win32"){let E=process.env.APPDATA??g(qE(),"AppData","Roaming");return g(E,"shotstack","log.jsonl")}return g(qE(),".shotstack","log.jsonl")}function kf(E,f=new Date){let R={ts:f.toISOString(),cmd:E.cmd,args:E.args.map(hf),exit:E.exit,durationMs:E.durationMs};if(E.renderId)R.renderId=E.renderId;if(E.response!==void 0)R.response=vf(n(E.response));if(E.error)R.error=Vf(E.error);return R}function Vf(E){return{name:E.name,message:typeof E.message==="string"?i(E.message):String(E.message)}}function Af(E){if(E instanceof Error)return{name:E.name,message:E.message};if(E===void 0||E===null)return;return{name:"Unknown",message:String(E)}}async function PE(E,f=A()){let R=kf(E),$=JSON.stringify(R)+`
|
|
24
|
+
`;if(await Kf(Wf(f),{recursive:!0,mode:448}),!await xf(f))await YE(f,"",{mode:384});await Hf(f,$),await bf(f)}async function xf(E){try{return await GE(E),!0}catch{return!1}}async function bf(E){let f=await GE(E).catch(()=>null);if(!f||f.size<=Df)return;let R=await Nf(E),$=R.subarray(Math.max(0,R.length-Ff)),S=$.indexOf(10),T=S>=0?$.subarray(S+1):$;await YE(E,T,{mode:384})}var Cf=/^https?:\/\//i,uf=/(https?:\/\/[^\s'"<>]+?)\?[^\s'"<>]*?(?:Signature|X-Amz-[A-Za-z0-9-]*)=[^\s'"<>]*/gi;function i(E){return E.replace(uf,"$1?[redacted-signed]")}function hf(E){if(typeof E!=="string")return E;if(E.startsWith("-"))return E;if(Cf.test(E))return i(E);if(/[\/\\]/.test(E))return Of(E);return E}function Of(E){let f=E.split(/[\/\\]/);return f[f.length-1]||E}function n(E){if(typeof E==="string")return i(E);if(Array.isArray(E))return E.map(n);if(E&&typeof E==="object"){let f={};for(let[R,$]of Object.entries(E))f[R]=n($);return f}return E}function vf(E){let f=JSON.stringify(E);if(f===void 0||f.length<=ME)return E;return f.slice(0,ME)+"...[truncated]"}async function x(E,f,R){let $=Date.now();try{let S=await R(),T=S.exitCode??0;if(await PE({cmd:E,args:f,exit:T,durationMs:Date.now()-$,renderId:S.renderId,response:S.response}).catch(()=>{}),T!==0)process.exit(T)}catch(S){let T=rf(S),L=S instanceof z?S.body:void 0;throw await PE({cmd:E,args:f,exit:T,durationMs:Date.now()-$,response:L,error:Af(S)}).catch(()=>{}),S}}function rf(E){if(E instanceof z)return E.isTransient?2:1;if(E instanceof Z||E instanceof B)return 1;if(E instanceof Error&&E.code==="ENOENT")return 1;return 2}function b(E){let f=process.argv.slice(2),R=f.indexOf(E);return R>=0?f.slice(R+1):f}var XE=new G("render").description("Submit a Shotstack timeline JSON to the render API").argument("<file>","Path to a Shotstack timeline JSON file").option("--env <name>",`Environment: ${j.join(" | ")}`).option("--output <format>","Output format: text | json","text").action(async(E,f)=>{await x("render",b("render"),async()=>{let R=k(f.output),$=D(),S=F(f.env),T=tf(process.cwd(),E),L=await sf(T,"utf8"),_=JSON.parse(L),w=await W({apiKey:$,env:S}).post("/render",_),y=w.response.id;return V(R,{id:y},y),{renderId:y,response:w.response}})});var cf=new Set(["done","failed"]),gf=3000,zE=new G("status").description("Poll the status of a Shotstack render").argument("<id>","Render ID returned by `shotstack render`").option("--watch","Poll continuously until the render reaches a terminal state").option("--env <name>",`Environment: ${j.join(" | ")}`).option("--output <format>","Output format: text | json","text").action(async(E,f)=>{await x("status",b("status"),async()=>{let R=k(f.output),$=D(),S=F(f.env),T=W({apiKey:$,env:S});while(!0){let _=(await T.get(`/render/${encodeURIComponent(E)}`)).response;if(V(R,_,nf(_)),!f.watch||cf.has(_.status)){let I=_.status==="failed"?1:0;return{renderId:_.id,response:_,exitCode:I}}await lf(gf)}})});function nf(E){let f=[E.status];if(E.url)f.push(E.url);if(E.error)f.push(`error: ${E.error}`);return f.join(" ")}function lf(E){return new Promise((f)=>setTimeout(f,E))}import{readFile as mf,writeFile as df,mkdtemp as pf}from"node:fs/promises";import{tmpdir as of}from"node:os";import{join as ZE}from"node:path";import{spawn as ef}from"node:child_process";var jE="https://github.com/shotstack/shotstack-cli/issues/new?template=bug.yml",af=6000,HE=5,NE=new G("feedback").description("Open a pre-filled GitHub issue with a flight-recorder dossier from your last 5 invocations").action(async()=>{let E=await E0(HE),f=f0(E),R=$0(E),$=`&title=${encodeURIComponent(R)}`,S=`${jE}${$}&body=${encodeURIComponent(f)}`;if(S.length<=af){KE(S),console.log("Opened browser with pre-filled issue. Review before submitting.");return}let T=await pf(ZE(of(),"shotstack-feedback-")),L=ZE(T,"dossier.md");await df(L,f),KE(`${jE}${$}`),console.log(`Dossier too large to inline. Saved to: ${L}`),console.log("Drag the file into the issue body, or paste its contents.")});async function E0(E){let f=A(),R;try{R=await mf(f,"utf8")}catch{return[]}let S=R.split(`
|
|
25
|
+
`).filter((L)=>L.trim().length>0).slice(-E).reverse(),T=[];for(let L of S)try{T.push(JSON.parse(L))}catch{}return T}function f0(E){let f=[];if(f.push("## Environment"),f.push(`- Shotstack CLI: ${H}`),f.push(`- OS: ${process.platform}-${process.arch}`),f.push(""),f.push(`## Last ${HE} invocations (most recent first)`),f.push(""),E.length===0)f.push("_No recorded invocations._");else for(let R of E){let $=(R.args??[]).join(" "),S=`### ${R.ts} — shotstack ${R.cmd}${$?" "+$:""}`;if(f.push(S),f.push(`- Exit: ${R.exit} Duration: ${R.durationMs}ms`),R.renderId)f.push(`- Render ID: ${R.renderId}`);if(R.response!==void 0){let T=typeof R.response==="string"?R.response:JSON.stringify(R.response);f.push(`- Response: ${T}`)}if(R.error)f.push(`- Error: ${R.error.name}: ${R.error.message}`);f.push("")}return f.push(`[full log at ${A()}]`),f.push(""),f.push("## What I expected vs what happened"),f.push("<!-- Describe the issue, request, or question here. -->"),f.join(`
|
|
26
|
+
`)}var BE=80,R0="Feedback from shotstack CLI";function $0(E){let f=E.find((S)=>S.error);if(f&&f.error)return l(`${f.cmd}: ${f.error.name} — ${f.error.message}`);let R=E.find((S)=>QE(S.response)!==null);if(R){let S=QE(R.response);return l(`Render failed: ${S}`)}let $=E.find((S)=>S.exit!==0);if($)return l(`${$.cmd} exited ${$.exit}`);return R0}function QE(E){if(!E||typeof E!=="object"||Array.isArray(E))return null;let f=E;if(f.state!=="failed")return null;return typeof f.error==="string"&&f.error.length>0?f.error:"unknown error"}function l(E){return E.length<=BE?E:E.slice(0,BE-1)+"…"}function KE(E){let f=process.platform==="win32",R=process.platform==="darwin"?"open":f?"cmd":"xdg-open",S=ef(R,f?["/c","start","",E]:[E],{stdio:"ignore",detached:!0});S.on("error",()=>{console.error(`Could not open browser. Visit: ${E}`)}),S.unref()}var S0=new G().name("shotstack").description("Command-line interface for the Shotstack video rendering API").version(H).addCommand(XE).addCommand(zE).addCommand(NE);try{await S0.parseAsync(process.argv)}catch(E){process.exit(T0(E))}function T0(E){if(E instanceof Z||E instanceof B)return console.error(E.message),1;if(E instanceof z){if(console.error(E.message),E.body)console.error(typeof E.body==="string"?E.body:JSON.stringify(E.body));return E.isTransient?2:1}if(E instanceof Error){if("code"in E&&E.code==="ENOENT")return console.error(E.message),1;return console.error(E.message),2}return console.error(String(E)),2}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shotstack/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line interface for the Shotstack video rendering API.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://github.com/shotstack/shotstack-cli",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/shotstack/shotstack-cli.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/shotstack/shotstack-cli/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"shotstack": "./dist/shotstack.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/shotstack.js",
|
|
20
|
+
"SKILL.md",
|
|
21
|
+
"references"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "bun run src/index.ts",
|
|
25
|
+
"build": "bun build src/index.ts --target=node --minify --outfile=dist/shotstack.js --banner='#!/usr/bin/env node' && chmod +x dist/shotstack.js",
|
|
26
|
+
"prepack": "bun run build",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "bun test"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^12.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/bun": "latest",
|
|
35
|
+
"typescript": "^5.6.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Placeholder asset library
|
|
2
|
+
|
|
3
|
+
Use these URLs when the user hasn't provided their own assets — for examples, tests, and demos. **This is not a curated stock library**; it's a small placeholder set hosted by Shotstack.
|
|
4
|
+
|
|
5
|
+
All URLs are public HTTPS and stable.
|
|
6
|
+
|
|
7
|
+
## Videos
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
https://shotstack-assets.s3.amazonaws.com/footage/beach.mp4
|
|
11
|
+
https://shotstack-assets.s3.amazonaws.com/footage/sunset.mp4
|
|
12
|
+
https://shotstack-assets.s3.amazonaws.com/footage/mountains.mp4
|
|
13
|
+
https://shotstack-assets.s3.amazonaws.com/footage/skateboarder.mp4
|
|
14
|
+
https://shotstack-assets.s3.amazonaws.com/footage/city-timelapse.mp4
|
|
15
|
+
https://shotstack-assets.s3.amazonaws.com/footage/drone.mp4
|
|
16
|
+
https://shotstack-assets.s3.amazonaws.com/footage/trees.mp4
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Images
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
https://shotstack-assets.s3.amazonaws.com/images/wave-barrel.jpg
|
|
23
|
+
https://shotstack-assets.s3.amazonaws.com/images/earth.jpg
|
|
24
|
+
https://shotstack-assets.s3.amazonaws.com/images/business-man.jpg
|
|
25
|
+
https://shotstack-assets.s3.amazonaws.com/images/financial-background.jpg
|
|
26
|
+
https://shotstack-assets.s3.amazonaws.com/images/happy1.jpg
|
|
27
|
+
https://shotstack-assets.s3.amazonaws.com/images/dog1.jpg
|
|
28
|
+
https://shotstack-assets.s3.amazonaws.com/images/waterfall.jpeg
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Music
|
|
32
|
+
|
|
33
|
+
Royalty-free tracks from the Unminus library, hosted on Shotstack's CDN. Suitable for `timeline.soundtrack` or as audio assets.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/white.mp3
|
|
37
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/waveform.mp3
|
|
38
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/reggae.mp3
|
|
39
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/palmtrees.mp3
|
|
40
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/neuron.mp3
|
|
41
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/lit.mp3
|
|
42
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/kring.mp3
|
|
43
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/happy.mp3
|
|
44
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/berlin.mp3
|
|
45
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/autumn.mp3
|
|
46
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/ambition.mp3
|
|
47
|
+
https://shotstack-assets.s3.amazonaws.com/music/unminus/ambisax.mp3
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Picking an asset
|
|
51
|
+
|
|
52
|
+
Match the placeholder to the user's intent when one isn't provided:
|
|
53
|
+
|
|
54
|
+
| Intent | Pick |
|
|
55
|
+
|---|---|
|
|
56
|
+
| Beach / vacation | `beach.mp4`, `sunset.mp4`, `palmtrees.mp3` |
|
|
57
|
+
| Cityscape / business | `city-timelapse.mp4`, `business-man.jpg`, `ambition.mp3` |
|
|
58
|
+
| Nature / outdoors | `mountains.mp4`, `drone.mp4`, `trees.mp4`, `waterfall.jpeg` |
|
|
59
|
+
| Sport / energy | `skateboarder.mp4`, `lit.mp3` |
|
|
60
|
+
| Earth / global | `earth.jpg`, `neuron.mp3` |
|
|
61
|
+
| Cheerful / upbeat | `happy1.jpg`, `dog1.jpg`, `happy.mp3`, `kring.mp3` |
|
|
62
|
+
| Editorial / corporate | `financial-background.jpg`, `berlin.mp3`, `white.mp3` |
|
|
63
|
+
|
|
64
|
+
When the user provides their own URLs, ignore this list entirely.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Fonts guide
|
|
2
|
+
|
|
3
|
+
The Shotstack render engine ships a small built-in font set; everything else must be loaded as a custom font via `timeline.fonts[]`. Per the [Shotstack docs](https://shotstack.io/docs/guide/architecting-an-application/rich-text/#custom-fonts), **prefer custom Google Fonts** for predictable rendering and full typographic range.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- The custom-font workflow (preferred)
|
|
8
|
+
- Built-in fonts (fallback)
|
|
9
|
+
- Why system fonts (Arial, Helvetica) fail
|
|
10
|
+
|
|
11
|
+
## Custom-font workflow (preferred)
|
|
12
|
+
|
|
13
|
+
Load a font from a public URL via `timeline.fonts[]`, then reference its **file basename** in the `font.family` property of any `rich-text` or `rich-caption` asset.
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"timeline": {
|
|
18
|
+
"fonts": [
|
|
19
|
+
{ "src": "https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm45xW5rygbi49c.ttf" }
|
|
20
|
+
],
|
|
21
|
+
"tracks": [
|
|
22
|
+
{
|
|
23
|
+
"clips": [{
|
|
24
|
+
"asset": {
|
|
25
|
+
"type": "rich-text",
|
|
26
|
+
"text": "Hello",
|
|
27
|
+
"font": { "family": "JTUSjIg1_i6t8kCHKm45xW5rygbi49c", "size": 60, "color": "#ffffff" }
|
|
28
|
+
},
|
|
29
|
+
"start": 0, "length": 3
|
|
30
|
+
}]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The `font.family` value is the URL's filename without extension.
|
|
38
|
+
|
|
39
|
+
For URL `https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm45xW5rygbi49c.ttf`, family = `JTUSjIg1_i6t8kCHKm45xW5rygbi49c`.
|
|
40
|
+
|
|
41
|
+
### Where to find Google Fonts URLs
|
|
42
|
+
|
|
43
|
+
The Shotstack Studio SDK ships a curated catalogue of ~400 Google Fonts at <https://github.com/shotstack/shotstack-studio-sdk/blob/main/src/core/fonts/google-fonts.ts>. Each entry has a fastest-served Google Fonts CDN URL.
|
|
44
|
+
|
|
45
|
+
Common picks:
|
|
46
|
+
|
|
47
|
+
| Font | URL |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Montserrat 700 | `https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm45xW5rygbi49c.ttf` |
|
|
50
|
+
| Roboto 400 | `https://fonts.gstatic.com/s/roboto/v48/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.ttf` |
|
|
51
|
+
| Inter 700 | `https://fonts.gstatic.com/s/inter/v20/UcCo3FwrK3iLTfvlaQc78lA2.ttf` |
|
|
52
|
+
| Bangers | `https://fonts.gstatic.com/s/bangers/v25/FeVQS0BTqb0h60ACL5la2bxii28.ttf` |
|
|
53
|
+
| Luckiest Guy | `https://fonts.gstatic.com/s/luckiestguy/v25/_gP_1RrxsjcxVyin9l9n_j2RStR3qDpraA.ttf` |
|
|
54
|
+
| Open Sans 400 | `https://fonts.gstatic.com/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc.ttf` |
|
|
55
|
+
| Poppins 700 | `https://fonts.gstatic.com/s/poppins/v22/pxiByp8kv8JHgFVrLCz7Z1xlFQ.ttf` |
|
|
56
|
+
|
|
57
|
+
For other fonts, find the family on <https://fonts.google.com>, view the CSS, and copy the `.ttf` URL from the `@font-face` `src`.
|
|
58
|
+
|
|
59
|
+
### Multiple weights / styles
|
|
60
|
+
|
|
61
|
+
Each weight or style is a separate URL — load each one you need:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"timeline": {
|
|
66
|
+
"fonts": [
|
|
67
|
+
{ "src": "https://fonts.gstatic.com/s/roboto/v48/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.ttf" },
|
|
68
|
+
{ "src": "https://fonts.gstatic.com/s/roboto/v48/KFOlCnqEu92Fr1MmEU9fBBc4.ttf" }
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Built-in fonts (fallback)
|
|
75
|
+
|
|
76
|
+
These twelve fonts are pre-installed in the render engine and don't need a `timeline.fonts[]` entry. Use them only when you can't load a custom font.
|
|
77
|
+
|
|
78
|
+
| Family | Weight | Italic? |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `Arapey` | 400 | yes |
|
|
81
|
+
| `Clear Sans` | 400 | no |
|
|
82
|
+
| `Didact Gothic` | 400 | no |
|
|
83
|
+
| `Montserrat` | 400 | yes (also as custom font) |
|
|
84
|
+
| `MovLette` | 400 | no |
|
|
85
|
+
| `NotoEmoji` | 400 | no — emoji glyphs |
|
|
86
|
+
| `Open Sans` | 400 | yes |
|
|
87
|
+
| `Permanent Marker` | 400 | no |
|
|
88
|
+
| `Roboto` | 400 | yes (incl. 700 italic, 800–900 italic) |
|
|
89
|
+
| `Sue Ellen Francisco` | 400 | no |
|
|
90
|
+
| `Uni Neue` | **700 only** | no |
|
|
91
|
+
| `Work Sans` | 400 | yes |
|
|
92
|
+
|
|
93
|
+
The renderer accepts both spaced (`Open Sans`) and concatenated (`OpenSans`) family names for these.
|
|
94
|
+
|
|
95
|
+
## Why system fonts fail
|
|
96
|
+
|
|
97
|
+
System fonts like `Arial`, `Helvetica`, `Times New Roman`, `Courier New`, etc. are **not installed** on the render engine. Using them produces a "Font not found" error at render time.
|
|
98
|
+
|
|
99
|
+
**Wrong:**
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{ "font": { "family": "Arial", "size": 48, "color": "#000000" } }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Right (custom):**
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"timeline": {
|
|
110
|
+
"fonts": [
|
|
111
|
+
{ "src": "https://fonts.gstatic.com/s/inter/v20/UcCo3FwrK3iLTfvlaQc78lA2.ttf" }
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
"asset": {
|
|
115
|
+
"font": { "family": "UcCo3FwrK3iLTfvlaQc78lA2", "size": 48, "color": "#000000" }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Right (built-in):**
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{ "font": { "family": "Roboto", "size": 48, "color": "#000000" } }
|
|
124
|
+
```
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Rich-caption guide
|
|
2
|
+
|
|
3
|
+
Word-level animated captions. The most failure-prone asset type — read this fully before composing one.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- The five named presets (use these verbatim when possible)
|
|
8
|
+
- Sizing per output resolution
|
|
9
|
+
- The `alias://` source pattern
|
|
10
|
+
- Required clip-level dimensions
|
|
11
|
+
- Custom styling (when no preset fits)
|
|
12
|
+
|
|
13
|
+
## The five named presets
|
|
14
|
+
|
|
15
|
+
These are the canonical Shotstack rich-caption styles, lifted from the dashboard-v2 Captions panel. Use one verbatim unless the user asks for something specific.
|
|
16
|
+
|
|
17
|
+
All presets assume:
|
|
18
|
+
- Output resolution `1080` (1920×1080) or `hd` (1280×720)
|
|
19
|
+
- The captions track is **above** the audio/video source track
|
|
20
|
+
- The source clip has `alias: "audio"` (or whatever you wire `src: "alias://<name>"` to)
|
|
21
|
+
|
|
22
|
+
If output is portrait (1080×1920) or another aspect, scale `width`, `height`, `font.size`, and `padding` proportionally to the output dimensions.
|
|
23
|
+
|
|
24
|
+
### Nico — bold uppercase, slide-down animation
|
|
25
|
+
|
|
26
|
+
White Montserrat, uppercase, with subtle drop shadow. Slides each word down on activation.
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"asset": {
|
|
31
|
+
"type": "rich-caption",
|
|
32
|
+
"src": "alias://audio",
|
|
33
|
+
"font": { "family": "JTUSjIg1_i6t8kCHKm45xW5rygbi49c", "size": 88, "color": "#ffffff", "opacity": 1, "weight": 700 },
|
|
34
|
+
"wordAnimation": { "style": "slide", "direction": "down" },
|
|
35
|
+
"border": { "width": 0, "color": "#000000", "opacity": 1, "radius": 18 },
|
|
36
|
+
"style": { "textTransform": "uppercase" },
|
|
37
|
+
"padding": { "top": 25, "right": 0, "bottom": 0, "left": 0 },
|
|
38
|
+
"shadow": { "offsetX": 5, "offsetY": 5, "blur": 4, "color": "#000000", "opacity": 0.59 },
|
|
39
|
+
"active": {
|
|
40
|
+
"shadow": { "offsetX": 5, "offsetY": 5, "blur": 4, "color": "#000000", "opacity": 0.59 }
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"start": 0, "length": "end",
|
|
44
|
+
"width": 800, "height": 200,
|
|
45
|
+
"offset": { "x": 0, "y": -0.35 }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Required `timeline.fonts[]` entry: `https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm45xW5rygbi49c.ttf`
|
|
50
|
+
|
|
51
|
+
### Kai — black-on-white pill, fade animation
|
|
52
|
+
|
|
53
|
+
Inter, black text on white pill background. Active word greys out. Clean editorial look.
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"asset": {
|
|
58
|
+
"type": "rich-caption",
|
|
59
|
+
"src": "alias://audio",
|
|
60
|
+
"font": { "family": "UcCo3FwrK3iLTfvlaQc78lA2", "size": 52, "color": "#000000", "opacity": 1, "weight": 700 },
|
|
61
|
+
"wordAnimation": { "style": "fade" },
|
|
62
|
+
"border": { "width": 0, "color": "#000000", "opacity": 1, "radius": 18 },
|
|
63
|
+
"style": { "textTransform": "none" },
|
|
64
|
+
"padding": { "top": 25, "right": 0, "bottom": 15, "left": 0 },
|
|
65
|
+
"active": { "font": { "color": "#7d7e80" } },
|
|
66
|
+
"background": { "color": "#ffffff", "opacity": 1, "borderRadius": 10 }
|
|
67
|
+
},
|
|
68
|
+
"start": 0, "length": "end",
|
|
69
|
+
"width": 678, "height": 143,
|
|
70
|
+
"offset": { "x": 0, "y": -0.3 }
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Required font: `https://fonts.gstatic.com/s/inter/v20/UcCo3FwrK3iLTfvlaQc78lA2.ttf`
|
|
75
|
+
|
|
76
|
+
### Kapow — comic-book stroke with purple highlight
|
|
77
|
+
|
|
78
|
+
Luckiest Guy, white with thick black stroke, rotated -7.5°, active word gets a purple background. Use for energetic / playful content.
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"asset": {
|
|
83
|
+
"type": "rich-caption",
|
|
84
|
+
"src": "alias://audio",
|
|
85
|
+
"font": { "family": "_gP_1RrxsjcxVyin9l9n_j2RStR3qDpraA", "size": 52, "color": "#ffffff", "opacity": 1, "weight": 700 },
|
|
86
|
+
"wordAnimation": { "style": "highlight" },
|
|
87
|
+
"border": { "width": 0, "color": "#000000", "opacity": 1, "radius": 18 },
|
|
88
|
+
"style": { "textTransform": "uppercase" },
|
|
89
|
+
"padding": { "top": 0, "right": 0, "bottom": 0, "left": 0 },
|
|
90
|
+
"stroke": { "width": 3, "color": "#000000", "opacity": 1 },
|
|
91
|
+
"active": {
|
|
92
|
+
"stroke": { "width": 3, "color": "#000000", "opacity": 1 },
|
|
93
|
+
"font": { "background": "#690be9" }
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"start": 0, "length": "end",
|
|
97
|
+
"width": 434, "height": 143,
|
|
98
|
+
"offset": { "x": 0, "y": 0 },
|
|
99
|
+
"transform": { "rotate": { "angle": -7.5 } }
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Required font: `https://fonts.gstatic.com/s/luckiestguy/v25/_gP_1RrxsjcxVyin9l9n_j2RStR3qDpraA.ttf`
|
|
104
|
+
|
|
105
|
+
### Lovely Little Lychee — soft pink highlight
|
|
106
|
+
|
|
107
|
+
Montserrat, white with thin stroke and shadow, active word in soft pink. Romantic / lifestyle content.
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"asset": {
|
|
112
|
+
"type": "rich-caption",
|
|
113
|
+
"src": "alias://audio",
|
|
114
|
+
"font": { "family": "JTUSjIg1_i6t8kCHKm45xW5rygbi49c", "size": 36, "color": "#ffffff", "opacity": 1, "weight": 700 },
|
|
115
|
+
"wordAnimation": { "style": "highlight" },
|
|
116
|
+
"border": { "width": 0, "color": "#000000", "opacity": 1, "radius": 18 },
|
|
117
|
+
"style": { "textTransform": "none" },
|
|
118
|
+
"padding": { "top": 15, "right": 0, "bottom": 0, "left": 0 },
|
|
119
|
+
"stroke": { "width": 1, "color": "#000000", "opacity": 1 },
|
|
120
|
+
"active": {
|
|
121
|
+
"font": { "color": "#ffcdd0" },
|
|
122
|
+
"stroke": { "width": 1, "color": "#000000", "opacity": 1 },
|
|
123
|
+
"shadow": { "offsetX": 5, "offsetY": 5, "blur": 4, "color": "#000000", "opacity": 0.8 }
|
|
124
|
+
},
|
|
125
|
+
"shadow": { "offsetX": 5, "offsetY": 5, "blur": 4, "color": "#000000", "opacity": 0.8 }
|
|
126
|
+
},
|
|
127
|
+
"start": 0, "length": "end",
|
|
128
|
+
"width": 609, "height": 65,
|
|
129
|
+
"offset": { "x": 0, "y": -0.4 }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Required font: same as Nico (Montserrat).
|
|
134
|
+
|
|
135
|
+
### Rizz — bold display with thick stroke and pop animation
|
|
136
|
+
|
|
137
|
+
Bangers, white with very thick black stroke, active word turns blue. High-energy social-media style.
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"asset": {
|
|
142
|
+
"type": "rich-caption",
|
|
143
|
+
"src": "alias://audio",
|
|
144
|
+
"font": { "family": "FeVQS0BTqb0h60ACL5la2bxii28", "size": 84, "color": "#ffffff", "opacity": 1, "weight": 700 },
|
|
145
|
+
"wordAnimation": { "style": "pop" },
|
|
146
|
+
"border": { "width": 0, "color": "#000000", "opacity": 1, "radius": 18 },
|
|
147
|
+
"style": { "textTransform": "none" },
|
|
148
|
+
"padding": { "top": 35, "right": 0, "bottom": 0, "left": 0 },
|
|
149
|
+
"stroke": { "width": 10, "color": "#000000", "opacity": 1 },
|
|
150
|
+
"active": {
|
|
151
|
+
"font": { "color": "#6375ff" },
|
|
152
|
+
"stroke": { "width": 10, "color": "#000000", "opacity": 1 }
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"start": 0, "length": "end",
|
|
156
|
+
"width": 609, "height": 167,
|
|
157
|
+
"offset": { "x": 0, "y": -0.35 }
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Required font: `https://fonts.gstatic.com/s/bangers/v25/FeVQS0BTqb0h60ACL5la2bxii28.ttf`
|
|
162
|
+
|
|
163
|
+
## Sizing per output resolution
|
|
164
|
+
|
|
165
|
+
The preset values above target full HD (1920×1080). For other output resolutions, scale `width`, `height`, `font.size`, and `padding` linearly:
|
|
166
|
+
|
|
167
|
+
| Output | Scale factor (vs 1080p) |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `preview` 512×288 | 0.27 |
|
|
170
|
+
| `mobile` 640×360 | 0.33 |
|
|
171
|
+
| `sd` 1024×576 | 0.53 |
|
|
172
|
+
| `hd` 1280×720 | 0.67 |
|
|
173
|
+
| `1080` 1920×1080 | 1.00 (preset values as-is) |
|
|
174
|
+
| Portrait 1080×1920 | Use `width: 900` and similar 1080-equivalents; the offset `y` may need adjustment for vertical framing |
|
|
175
|
+
|
|
176
|
+
Always render at least once at the target resolution and verify the captions are positioned correctly.
|
|
177
|
+
|
|
178
|
+
## The `alias://` source pattern
|
|
179
|
+
|
|
180
|
+
Rich-caption transcribes audio to produce word-level timing. Three ways to source it:
|
|
181
|
+
|
|
182
|
+
| `src` value | Behaviour |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `alias://<name>` | Auto-transcribe a referenced audio/video/text-to-speech clip. Set `alias: "<name>"` on the source clip. **Preferred for sync.** |
|
|
185
|
+
| Subtitle file URL | Use existing `.srt`, `.vtt`, `.ttml`, or `.dfxp` file. No auto-transcription. |
|
|
186
|
+
| Audio/video file URL | Auto-transcribe a standalone media file. Use when there's no source clip on the timeline. |
|
|
187
|
+
|
|
188
|
+
The `alias://` form keeps the captions in sync with the source clip even when you change its `start` or `length`.
|
|
189
|
+
|
|
190
|
+
## Required clip-level dimensions
|
|
191
|
+
|
|
192
|
+
A `rich-caption` clip without `width`, `height`, and `fit: "none"` defaults to filling the entire output frame, which obscures everything below. The five presets all set these explicitly. If you author your own:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"asset": { "type": "rich-caption", "...": "..." },
|
|
197
|
+
"start": 0, "length": "end",
|
|
198
|
+
"width": 678, "height": 143,
|
|
199
|
+
"fit": "none",
|
|
200
|
+
"offset": { "x": 0, "y": -0.3 }
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`offset.y: -0.3` puts the captions in the lower-third area. Negative `y` is below centre; positive is above.
|
|
205
|
+
|
|
206
|
+
## Custom styling
|
|
207
|
+
|
|
208
|
+
If none of the five presets fits, copy the closest preset and modify. The properties are:
|
|
209
|
+
|
|
210
|
+
| Group | Properties |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `font` | `family`, `size`, `color`, `opacity`, `weight` |
|
|
213
|
+
| `wordAnimation` | `style`: `fade`, `slide`, `highlight`, `pop`. `direction` for slide: `up`, `down`, `left`, `right`. |
|
|
214
|
+
| `border` | `width`, `color`, `opacity`, `radius` |
|
|
215
|
+
| `padding` | `top`, `right`, `bottom`, `left` (numbers) |
|
|
216
|
+
| `stroke` | `width`, `color`, `opacity` (text outline) |
|
|
217
|
+
| `shadow` | `offsetX`, `offsetY`, `blur`, `color`, `opacity` |
|
|
218
|
+
| `background` | `color`, `opacity`, `borderRadius` (the rounded pill behind text) |
|
|
219
|
+
| `active` | Same property groups, applied only to the currently-spoken word |
|
|
220
|
+
| `style.textTransform` | `none`, `uppercase`, `lowercase`, `capitalize` |
|
|
221
|
+
|
|
222
|
+
For the canonical schema, see <https://shotstack.io/docs/api/#tocs_richcaptionasset>.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# SVG asset guide
|
|
2
|
+
|
|
3
|
+
The `svg` asset type embeds raw SVG markup into a clip. Use it for vector shapes, icons, and decorative elements. **Use `rich-text` for any text content** — SVG `<text>` is unsupported.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- Required attributes
|
|
8
|
+
- Supported elements
|
|
9
|
+
- Unsupported elements (and what to use instead)
|
|
10
|
+
- Worked example
|
|
11
|
+
|
|
12
|
+
## Required attributes
|
|
13
|
+
|
|
14
|
+
Every SVG asset's `src` is raw markup (not a URL) and **must include all four** of:
|
|
15
|
+
|
|
16
|
+
- `xmlns="http://www.w3.org/2000/svg"`
|
|
17
|
+
- `viewBox` defining the coordinate space
|
|
18
|
+
- `width` (in pixels)
|
|
19
|
+
- `height` (in pixels)
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"asset": {
|
|
24
|
+
"type": "svg",
|
|
25
|
+
"src": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1280 720\" width=\"1280\" height=\"720\"><circle cx=\"640\" cy=\"360\" r=\"80\" fill=\"#e74c3c\"/></svg>"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Without these, the renderer can't size or position the SVG.
|
|
31
|
+
|
|
32
|
+
## Supported elements
|
|
33
|
+
|
|
34
|
+
The renderer understands these SVG primitives:
|
|
35
|
+
|
|
36
|
+
- `<path>` — arbitrary vector paths via `d` attribute
|
|
37
|
+
- `<rect>` — rectangles
|
|
38
|
+
- `<circle>` — circles
|
|
39
|
+
- `<ellipse>` — ellipses
|
|
40
|
+
- `<line>` — straight lines
|
|
41
|
+
- `<polygon>` — closed polygons
|
|
42
|
+
- `<polyline>` — open polygonal lines
|
|
43
|
+
- `<g>` — grouping (for transforms applied to multiple shapes)
|
|
44
|
+
|
|
45
|
+
Standard fill, stroke, and transform attributes work: `fill`, `fill-opacity`, `stroke`, `stroke-width`, `stroke-opacity`, `transform`.
|
|
46
|
+
|
|
47
|
+
## Unsupported elements
|
|
48
|
+
|
|
49
|
+
| Element | Use instead |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `<text>` | `rich-text` asset |
|
|
52
|
+
| `<image>` | `image` asset |
|
|
53
|
+
| `<animate>`, `<animateTransform>` | Static SVG; use clip-level `transform` for motion |
|
|
54
|
+
| `<foreignObject>` | Not supported |
|
|
55
|
+
| `<filter>` (most filters) | Apply effects via clip-level properties or post-process |
|
|
56
|
+
| External `<use>` references | Inline the markup |
|
|
57
|
+
|
|
58
|
+
Animated SVGs are not rendered — only the static initial frame is used.
|
|
59
|
+
|
|
60
|
+
## Worked example: lower-third bar
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"timeline": {
|
|
65
|
+
"tracks": [
|
|
66
|
+
{
|
|
67
|
+
"clips": [{
|
|
68
|
+
"asset": {
|
|
69
|
+
"type": "rich-text",
|
|
70
|
+
"text": "Jane Smith — CEO",
|
|
71
|
+
"font": { "family": "JTUSjIg1_i6t8kCHKm45xW5rygbi49c", "size": 48, "color": "#ffffff" }
|
|
72
|
+
},
|
|
73
|
+
"start": 0, "length": 5,
|
|
74
|
+
"offset": { "x": -0.2, "y": -0.35 }
|
|
75
|
+
}]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"clips": [{
|
|
79
|
+
"asset": {
|
|
80
|
+
"type": "svg",
|
|
81
|
+
"src": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1920 200\" width=\"1920\" height=\"200\"><rect x=\"0\" y=\"0\" width=\"1920\" height=\"200\" fill=\"#000000\" fill-opacity=\"0.7\"/></svg>"
|
|
82
|
+
},
|
|
83
|
+
"start": 0, "length": 5,
|
|
84
|
+
"offset": { "x": 0, "y": -0.35 }
|
|
85
|
+
}]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"clips": [{
|
|
89
|
+
"asset": { "type": "video", "src": "https://shotstack-assets.s3.amazonaws.com/footage/beach.mp4" },
|
|
90
|
+
"start": 0, "length": 5
|
|
91
|
+
}]
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"output": { "format": "mp4", "resolution": "1080" }
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Track order: text on top, semi-transparent black bar in the middle, video on the bottom.
|
|
100
|
+
|
|
101
|
+
## When to use SVG vs rich-text
|
|
102
|
+
|
|
103
|
+
| Need | Asset |
|
|
104
|
+
|---|---|
|
|
105
|
+
| Any words, letters, or characters | `rich-text` |
|
|
106
|
+
| Decorative shapes, lines, icons | `svg` |
|
|
107
|
+
| Animated text effects | `rich-text` (with the asset's animation properties) or `rich-caption` |
|
|
108
|
+
| Logo with shapes only | `svg` |
|
|
109
|
+
| Logo with text + shapes | `rich-text` for the text + `svg` for shapes, on adjacent tracks |
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Timeline conventions
|
|
2
|
+
|
|
3
|
+
Detailed guide to track layering, the soundtrack vs audio distinction, and `timeline.fonts[]`. Read this when building any non-trivial timeline JSON.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- Track layering (with worked example)
|
|
8
|
+
- Soundtrack vs audio asset
|
|
9
|
+
- `timeline.fonts[]` for custom fonts
|
|
10
|
+
- Background colour
|
|
11
|
+
- Cache control
|
|
12
|
+
- Common track-layout patterns
|
|
13
|
+
|
|
14
|
+
## Track layering
|
|
15
|
+
|
|
16
|
+
The single most counter-intuitive Shotstack convention.
|
|
17
|
+
|
|
18
|
+
`timeline.tracks` is an array. **The first element is the TOP layer (foreground); the last is the BOTTOM (background).** The order of the JSON array IS the visual stacking order, top-to-bottom.
|
|
19
|
+
|
|
20
|
+
Per the [official docs](https://shotstack.io/docs/guide/architecting-an-application/guidelines/):
|
|
21
|
+
|
|
22
|
+
> Tracks are layered on top of each other in the same order they are added to the array with the top most track layered over the top of those below it.
|
|
23
|
+
|
|
24
|
+
### Worked example: caption on a video
|
|
25
|
+
|
|
26
|
+
To overlay captions on a video:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"timeline": {
|
|
31
|
+
"tracks": [
|
|
32
|
+
{
|
|
33
|
+
"clips": [{
|
|
34
|
+
"asset": { "type": "rich-caption", "src": "alias://voice", "...": "..." },
|
|
35
|
+
"start": 0, "length": "end",
|
|
36
|
+
"width": 678, "height": 143,
|
|
37
|
+
"fit": "none"
|
|
38
|
+
}]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"clips": [{
|
|
42
|
+
"asset": { "type": "audio", "src": "https://.../voice.mp3" },
|
|
43
|
+
"start": 0, "length": "auto",
|
|
44
|
+
"alias": "voice"
|
|
45
|
+
}]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"clips": [{
|
|
49
|
+
"asset": { "type": "video", "src": "https://.../background.mp4" },
|
|
50
|
+
"start": 0, "length": "end"
|
|
51
|
+
}]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"output": { "format": "mp4", "resolution": "hd" }
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Order of tracks:
|
|
60
|
+
1. **Top:** `rich-caption` (the visible captions)
|
|
61
|
+
2. **Middle:** `audio` (provides the words for transcription via `alias`)
|
|
62
|
+
3. **Bottom:** `video` (the background imagery)
|
|
63
|
+
|
|
64
|
+
If you put the video first and the captions last, the video covers the captions and you see nothing.
|
|
65
|
+
|
|
66
|
+
### When to use multiple tracks
|
|
67
|
+
|
|
68
|
+
| Goal | Layout |
|
|
69
|
+
|---|---|
|
|
70
|
+
| Background + foreground overlay | Two tracks |
|
|
71
|
+
| Multiple parallel audio sources (voiceover + music) | Soundtrack for music, separate audio track for voiceover |
|
|
72
|
+
| Title card on video | Title clip in early track, video in later track |
|
|
73
|
+
| Picture-in-picture | PiP video in early track, main video in later track |
|
|
74
|
+
|
|
75
|
+
## Soundtrack vs audio asset
|
|
76
|
+
|
|
77
|
+
There are two ways to add audio. Pick the right one.
|
|
78
|
+
|
|
79
|
+
| Use `timeline.soundtrack` when | Use `audio` asset when |
|
|
80
|
+
|---|---|
|
|
81
|
+
| One background music track for the entire timeline | Sound effects at specific times |
|
|
82
|
+
| Loop or fade the music | Voiceover synced to specific clips |
|
|
83
|
+
| You don't need to control timing | Multiple audio sources at different times |
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"timeline": {
|
|
88
|
+
"soundtrack": {
|
|
89
|
+
"src": "https://shotstack-assets.s3.amazonaws.com/music/unminus/palmtrees.mp3",
|
|
90
|
+
"effect": "fadeOut"
|
|
91
|
+
},
|
|
92
|
+
"tracks": [/* ... */]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Soundtrack supported `effect` values: `fadeIn`, `fadeOut`, `fadeInFadeOut`.
|
|
98
|
+
|
|
99
|
+
## `timeline.fonts[]` for custom fonts
|
|
100
|
+
|
|
101
|
+
Custom fonts loaded from a URL, available to all `rich-text` and `rich-caption` clips in the timeline.
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"timeline": {
|
|
106
|
+
"fonts": [
|
|
107
|
+
{ "src": "https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm45xW5rygbi49c.ttf" },
|
|
108
|
+
{ "src": "https://fonts.gstatic.com/s/bangers/v25/FeVQS0BTqb0h60ACL5la2bxii28.ttf" }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The `font.family` you reference in an asset must be the **basename of the file** (without `.ttf`/`.otf`/`.woff`).
|
|
115
|
+
|
|
116
|
+
For the Montserrat URL above, `font.family` is `JTUSjIg1_i6t8kCHKm45xW5rygbi49c`.
|
|
117
|
+
|
|
118
|
+
See `references/fonts.md` for the full pattern.
|
|
119
|
+
|
|
120
|
+
## Background colour
|
|
121
|
+
|
|
122
|
+
`timeline.background` is a hex string. Defaults to `#000000`.
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{ "timeline": { "background": "#ffffff", "tracks": [/* ... */] } }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Cache control
|
|
129
|
+
|
|
130
|
+
`timeline.cache: false` disables caching of fetched source assets for this render. Default is `true`. Disable cache when you're iterating on the same source URL with changed content.
|
|
131
|
+
|
|
132
|
+
## Common track-layout patterns
|
|
133
|
+
|
|
134
|
+
**Slideshow:** images in one track, soundtrack for music. One image per clip; use `start: "auto"` to chain them.
|
|
135
|
+
|
|
136
|
+
**Voiceover with captions:** captions in track[0], audio in track[1] (with `alias` for the captions to reference), video/image in track[2].
|
|
137
|
+
|
|
138
|
+
**Lower third title:** title `rich-text` clip in track[0] with positioned `offset`, main video in track[1].
|
|
139
|
+
|
|
140
|
+
**Picture-in-picture:** small video with `scale: 0.3` in track[0] with `offset`, main video at full size in track[1].
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Common errors and the fix for each. If your error isn't here, run `shotstack feedback` to file a pre-filled GitHub issue with the render UUID attached — engineers can find server-side state in seconds.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- "Unknown property: alignment" / wrong property names
|
|
8
|
+
- "Invalid option: expected one of top|middle|bottom"
|
|
9
|
+
- Video looks stretched / squished after rendering
|
|
10
|
+
- "Font not found"
|
|
11
|
+
- Captions cover the whole frame
|
|
12
|
+
- Timeline renders but layers are wrong
|
|
13
|
+
- "Invalid asset URL"
|
|
14
|
+
- "Render failed" with no other detail
|
|
15
|
+
- Render takes much longer than expected
|
|
16
|
+
- Stage credits exhausted
|
|
17
|
+
|
|
18
|
+
## "Unknown property: alignment" (and similar)
|
|
19
|
+
|
|
20
|
+
You used a property name from CSS or HTML instinct — `alignment`, `font.name`, `duration` — instead of the Shotstack name.
|
|
21
|
+
|
|
22
|
+
**Fix:** the spec uses these exact names:
|
|
23
|
+
|
|
24
|
+
| You'd guess (wrong) | API uses (right) |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `alignment` | `align` |
|
|
27
|
+
| `font.name` | `font.family` |
|
|
28
|
+
| `duration` | `length` |
|
|
29
|
+
| `transitions: [...]` (array) | `transition: { in, out }` (object) |
|
|
30
|
+
|
|
31
|
+
Always check <https://shotstack.io/docs/api/api.bundled.json> (JSON Schema) or <https://shotstack.io/docs/guide/llms-full.txt> before composing timeline JSON. Don't guess from CSS conventions.
|
|
32
|
+
|
|
33
|
+
## "Invalid option: expected one of top|middle|bottom"
|
|
34
|
+
|
|
35
|
+
You set `align.vertical` to `"center"` (CSS instinct). Shotstack's vertical enum is `top | middle | bottom`. Horizontal is `left | center | right` (the word "center" works there, just not for vertical).
|
|
36
|
+
|
|
37
|
+
**Fix:** use `"middle"` for vertical centering.
|
|
38
|
+
|
|
39
|
+
## Video looks stretched / squished after rendering
|
|
40
|
+
|
|
41
|
+
You used `fit: "cover"` from CSS instinct expecting it to scale-and-crop maintaining aspect ratio. Shotstack's `cover` does the opposite — it STRETCHES the asset to fill the viewport without maintaining aspect ratio.
|
|
42
|
+
|
|
43
|
+
The Shotstack `fit` enum is **inverted from CSS**:
|
|
44
|
+
|
|
45
|
+
| Shotstack value | Behaviour |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `crop` (default) | Scale to fill, **maintaining aspect ratio**, crop excess. This is what CSS calls `object-fit: cover`. |
|
|
48
|
+
| `cover` | Stretch to fill, ignoring aspect ratio. This is what CSS calls `object-fit: fill`. |
|
|
49
|
+
| `contain` | Fit entirely within viewport, maintaining aspect ratio (letterbox). |
|
|
50
|
+
| `none` | No scaling. |
|
|
51
|
+
|
|
52
|
+
**Fix:** use `fit: "crop"` (or omit `fit` — `crop` is the default) for the typical "scale and crop" behaviour you'd expect from CSS `object-fit: cover`.
|
|
53
|
+
|
|
54
|
+
## "Font not found"
|
|
55
|
+
|
|
56
|
+
You used a font name that isn't in the built-in list and didn't load it via `timeline.fonts[]`.
|
|
57
|
+
|
|
58
|
+
**Fix:** add the font URL to `timeline.fonts[]` and use the file basename as `family`. See `references/fonts.md`. If you really need a system font, use `Roboto` (built-in) as a substitute.
|
|
59
|
+
|
|
60
|
+
## Captions cover the whole frame
|
|
61
|
+
|
|
62
|
+
A `rich-caption` clip without `width`, `height`, and `fit: "none"` defaults to filling the entire output.
|
|
63
|
+
|
|
64
|
+
**Fix:** use one of the five named presets in `references/rich-caption.md` (Nico, Kai, Kapow, Lovely Little Lychee, Rizz) which include the right dimensions, or set `width`/`height`/`fit: "none"` explicitly on your clip.
|
|
65
|
+
|
|
66
|
+
## Timeline renders but layers are wrong
|
|
67
|
+
|
|
68
|
+
You assumed `tracks[0]` was the bottom layer.
|
|
69
|
+
|
|
70
|
+
**Fix:** reverse the track array. `tracks[0]` is the TOP layer; the last track is the BOTTOM. Captions and overlays go in early tracks; backgrounds go in late tracks. See `references/timeline.md`.
|
|
71
|
+
|
|
72
|
+
## "Invalid asset URL"
|
|
73
|
+
|
|
74
|
+
The `src` is not a public HTTPS URL, or it's a `data:` URI, or it's a local path, or the URL needs auth that isn't included.
|
|
75
|
+
|
|
76
|
+
**Fix:** host the asset at a public HTTPS URL or use a presigned URL with credentials in the URL itself. For tests, pull from `references/asset-library.md`.
|
|
77
|
+
|
|
78
|
+
## "Render failed" with no other detail
|
|
79
|
+
|
|
80
|
+
`shotstack status <id> --output json` returns the full response including the error message. The `--watch` text view truncates.
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
shotstack status <id> --output json | jq .response.error
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then run `shotstack feedback` — the dossier captures the response body and the engineer can correlate against server-side logs in <30 seconds.
|
|
87
|
+
|
|
88
|
+
## Render takes much longer than expected
|
|
89
|
+
|
|
90
|
+
Renders containing AI-generation assets (`text-to-image`, `image-to-video`) take longer because the engine waits for the AI job to complete before rendering. Renders with very long source videos or many tracks also take longer.
|
|
91
|
+
|
|
92
|
+
**Fix:** if you're iterating quickly, render with `output.resolution: "preview"` first (512×288 @ 15fps) to validate the timeline shape before spending credits on full-resolution output.
|
|
93
|
+
|
|
94
|
+
## Stage credits exhausted
|
|
95
|
+
|
|
96
|
+
The `stage` environment uses test credits. They reset on a regular cadence but can be exhausted if you run many renders.
|
|
97
|
+
|
|
98
|
+
**Fix:** wait for the reset, or run against `--env v1` (production credits, real money). Check current credit balance at <https://dashboard.shotstack.io>.
|
|
99
|
+
|
|
100
|
+
## Filing a useful bug report
|
|
101
|
+
|
|
102
|
+
`shotstack feedback` opens a pre-filled GitHub issue with:
|
|
103
|
+
|
|
104
|
+
- CLI version + OS
|
|
105
|
+
- Last 5 invocations (sanitised — no API key, no signed URL params)
|
|
106
|
+
- Render UUIDs and status responses
|
|
107
|
+
|
|
108
|
+
This dossier lets engineers find the server-side render state in seconds. **Always include the render UUID** when filing — without it, debugging is "user said it didn't work" with no way to reproduce.
|