Skip to content

Commit 1f9cd67

Browse files
committed
feat: add --watch option to render command for polling until completion
1 parent 76ec5e0 commit 1f9cd67

5 files changed

Lines changed: 48 additions & 28 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ SHOTSTACK_ENV=stage shotstack render template.json
5050

5151
### `shotstack render <file>`
5252

53-
Submits a Shotstack Edit JSON to the render API. Returns a render ID.
53+
Submits a Shotstack Edit JSON to the render API. Returns a render ID. With `--watch`, polls until the render reaches a terminal state and prints the output URL — equivalent to `render` followed by `status --watch` but in one command.
5454

5555
```sh
5656
shotstack render my-template.json
5757
shotstack render my-template.json --output json
58+
shotstack render my-template.json --watch # submit + poll until done
59+
shotstack render my-template.json --watch --output json
5860
```
5961

6062
### `shotstack status <id>`

SKILL.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,17 @@ Use `--env stage` for experimentation. Stage is free; v1 charges real credits. O
3232
## Quickstart
3333

3434
```sh
35-
# Submit. Use --output json so the ID is parseable.
36-
shotstack render template.json --output json --env stage
35+
# Submit + poll in one command (most agent flows want this).
36+
shotstack render template.json --watch
37+
# → done https://shotstack-api-v1-output.s3.amazonaws.com/.../01ja7-x8m2k-39rzv-cmvxve.mp4
38+
39+
# Submit only (returns ID).
40+
shotstack render template.json --output json
3741
# → {"id":"01ja7-x8m2k-39rzv-cmvxve"}
3842

39-
# Poll until terminal state.
40-
shotstack status 01ja7-x8m2k-39rzv-cmvxve --watch --env stage
41-
# → done https://shotstack-api-stage-output.s3.amazonaws.com/.../01ja7-x8m2k-39rzv-cmvxve.mp4
43+
# Poll an existing render to terminal state.
44+
shotstack status 01ja7-x8m2k-39rzv-cmvxve --watch
45+
# → done https://shotstack-api-v1-output.s3.amazonaws.com/.../01ja7-x8m2k-39rzv-cmvxve.mp4
4246
```
4347

4448
## Hand-off to a human before rendering
@@ -57,11 +61,11 @@ Use `render` only when you're confident the JSON is final, or there's no human t
5761

5862
## Four CLI rules
5963

60-
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.
64+
1. **Pipe → `--output json`.** Default output is human-readable. When parsing programmatically or piping to another command, always pass `--output json`.
6165

62-
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`.
66+
2. **Use `--watch`, not a polling loop.** `shotstack render <file> --watch` submits and polls in one shot; `shotstack status <id> --watch` polls an existing render. Both exit when terminal: `done` (exit 0) or `failed` (exit 1). Don't write `while true; do ...; sleep 3; done`.
6367

64-
3. **Fetch the current docs before generating Edit 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 an Edit from scratch.
68+
3. **Fetch the current schema and docs before generating Edit JSON.** The Shotstack API evolves; LLM training data is often stale. Pull <https://shotstack.io/docs/api/api.edit.json> and <https://shotstack.io/docs/guide/llms-full.txt> for the current schema and guides before composing an Edit from scratch.
6569

6670
4. **Hand off to a human via `preview` when uncertain.** Don't burn render credits iterating. Generate JSON → `shotstack preview` → human reviews/tweaks → render only when right.
6771

src/commands/render.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { requireApiKey } from "../http/auth.ts";
66
import { resolveEnv, ENV_NAMES } from "../http/env.ts";
77
import { emit, parseOutputFormat } from "../output.ts";
88
import { withRecording, commandArgv } from "../recorder.ts";
9+
import { pollStatus } from "./status.ts";
910

1011
interface RenderResponse {
1112
success: boolean;
@@ -17,8 +18,9 @@ export const renderCommand = new Command("render")
1718
.description("Submit a Shotstack Edit JSON to the render API")
1819
.argument("<file>", "Path to a Shotstack Edit JSON file")
1920
.option(`--env <name>`, `Environment: ${ENV_NAMES.join(" | ")}`)
21+
.option("--watch", "After submitting, poll until the render reaches a terminal state")
2022
.option("--output <format>", "Output format: text | json", "text")
21-
.action(async (file: string, options: { env?: string; output: string }) => {
23+
.action(async (file: string, options: { env?: string; watch?: boolean; output: string }) => {
2224
await withRecording("render", commandArgv("render"), async () => {
2325
const format = parseOutputFormat(options.output);
2426
const apiKey = requireApiKey();
@@ -32,7 +34,13 @@ export const renderCommand = new Command("render")
3234
const result = await client.post<RenderResponse>("/render", template);
3335
const id = result.response.id;
3436

35-
emit(format, { id }, id);
36-
return { renderId: id, response: result.response };
37+
if (!options.watch) {
38+
emit(format, { id }, id);
39+
return { renderId: id, response: result.response };
40+
}
41+
42+
const final = await pollStatus(client, id, format, true);
43+
const exitCode = final.status === "failed" ? 1 : 0;
44+
return { renderId: id, response: final, exitCode };
3745
});
3846
});

src/commands/status.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Command } from "commander";
2-
import { createClient } from "../http/client.ts";
2+
import { createClient, type Client } from "../http/client.ts";
33
import { requireApiKey } from "../http/auth.ts";
44
import { resolveEnv, ENV_NAMES } from "../http/env.ts";
5-
import { emit, parseOutputFormat } from "../output.ts";
5+
import { emit, type OutputFormat, parseOutputFormat } from "../output.ts";
66
import { withRecording, commandArgv } from "../recorder.ts";
77

8-
interface StatusResponse {
8+
export interface StatusResponse {
99
success: boolean;
1010
message: string;
1111
response: {
@@ -18,6 +18,8 @@ interface StatusResponse {
1818
};
1919
}
2020

21+
export type RenderStatus = StatusResponse["response"];
22+
2123
const TERMINAL_STATES = new Set(["done", "failed"]);
2224
const POLL_INTERVAL_MS = 3000;
2325

@@ -33,22 +35,24 @@ export const statusCommand = new Command("status")
3335
const apiKey = requireApiKey();
3436
const env = resolveEnv(options.env);
3537
const client = createClient({ apiKey, env });
36-
37-
while (true) {
38-
const result = await client.get<StatusResponse>(`/render/${encodeURIComponent(id)}`);
39-
const r = result.response;
40-
emit(format, r, formatHuman(r));
41-
42-
if (!options.watch || TERMINAL_STATES.has(r.status)) {
43-
const exitCode = r.status === "failed" ? 1 : 0;
44-
return { renderId: r.id, response: r, exitCode };
45-
}
46-
await sleep(POLL_INTERVAL_MS);
47-
}
38+
const final = await pollStatus(client, id, format, options.watch === true);
39+
const exitCode = final.status === "failed" ? 1 : 0;
40+
return { renderId: final.id, response: final, exitCode };
4841
});
4942
});
5043

51-
function formatHuman(r: StatusResponse["response"]): string {
44+
export async function pollStatus(client: Client, id: string, format: OutputFormat, watch: boolean): Promise<RenderStatus> {
45+
while (true) {
46+
const result = await client.get<StatusResponse>(`/render/${encodeURIComponent(id)}`);
47+
const r = result.response;
48+
emit(format, r, formatHuman(r));
49+
50+
if (!watch || TERMINAL_STATES.has(r.status)) return r;
51+
await sleep(POLL_INTERVAL_MS);
52+
}
53+
}
54+
55+
function formatHuman(r: RenderStatus): string {
5256
const parts: string[] = [r.status];
5357
if (r.url) parts.push(r.url);
5458
if (r.error) parts.push(`error: ${r.error}`);

src/http/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface ClientOptions {
2121
env: ResolvedEnv;
2222
}
2323

24+
export type Client = ReturnType<typeof createClient>;
25+
2426
export function createClient(options: ClientOptions) {
2527
const baseUrl = options.env.baseUrl.replace(/\/$/, "");
2628

0 commit comments

Comments
 (0)