@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 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.