rollberry 0.1.9 → 0.2.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/dist/options.js CHANGED
@@ -3,12 +3,12 @@ import { parseArgs } from 'node:util';
3
3
  import { MAX_FPS } from './capture/constants.js';
4
4
  import { parseCaptureUrl, validateHideSelector } from './capture/utils.js';
5
5
  import { VERSION } from './version.js';
6
- const DEFAULT_OUT_FILE = 'rollberry.mp4';
7
- const DEFAULT_VIEWPORT = '1440x900';
8
- const DEFAULT_FPS = 60;
9
- const DEFAULT_DURATION = 'auto';
10
- const DEFAULT_MOTION = 'ease-in-out-sine';
11
- const DEFAULT_TIMEOUT_MS = 30_000;
6
+ export const DEFAULT_OUT_FILE = 'rollberry.mp4';
7
+ export const DEFAULT_VIEWPORT = '1440x900';
8
+ export const DEFAULT_FPS = 60;
9
+ export const DEFAULT_DURATION = 'auto';
10
+ export const DEFAULT_MOTION = 'ease-in-out-sine';
11
+ export const DEFAULT_TIMEOUT_MS = 30_000;
12
12
  export class CliError extends Error {
13
13
  showUsage;
14
14
  constructor(message, showUsage = false) {
@@ -30,7 +30,7 @@ export class VersionRequest extends Error {
30
30
  this.name = 'VersionRequest';
31
31
  }
32
32
  }
33
- export function parseCliArgs(argv = process.argv.slice(2)) {
33
+ export function parseCommandArgs(argv = process.argv.slice(2)) {
34
34
  const [command, ...rest] = argv;
35
35
  if (command === '--help' || command === '-h') {
36
36
  throw new HelpRequest(formatUsage());
@@ -39,120 +39,32 @@ export function parseCliArgs(argv = process.argv.slice(2)) {
39
39
  throw new VersionRequest();
40
40
  }
41
41
  if (!command) {
42
- throw new CliError('A subcommand is required.\n\nAvailable commands:\n capture Capture a scroll video from one or more URLs\n\nRun rollberry --help for more information.');
43
- }
44
- if (command !== 'capture') {
45
- throw new CliError(`Unknown subcommand: ${command}\n\nAvailable commands:\n capture Capture a scroll video from one or more URLs\n\nRun rollberry --help for more information.`);
46
- }
47
- if (rest.includes('--help') || rest.includes('-h')) {
48
- throw new HelpRequest(formatUsage());
42
+ throw createMissingSubcommandError();
49
43
  }
50
- const parsed = parseArgs({
51
- args: rest,
52
- allowPositionals: true,
53
- options: {
54
- out: {
55
- type: 'string',
56
- },
57
- viewport: {
58
- type: 'string',
59
- },
60
- fps: {
61
- type: 'string',
62
- },
63
- duration: {
64
- type: 'string',
65
- },
66
- motion: {
67
- type: 'string',
68
- },
69
- timeout: {
70
- type: 'string',
71
- },
72
- 'wait-for': {
73
- type: 'string',
74
- },
75
- 'hide-selector': {
76
- type: 'string',
77
- multiple: true,
78
- },
79
- 'debug-frames-dir': {
80
- type: 'string',
81
- },
82
- 'page-gap': {
83
- type: 'string',
84
- },
85
- manifest: {
86
- type: 'string',
87
- },
88
- 'log-file': {
89
- type: 'string',
90
- },
91
- force: {
92
- type: 'boolean',
93
- },
94
- },
95
- strict: true,
96
- });
97
- if (parsed.positionals.length === 0) {
98
- throw new CliError('capture requires at least one URL.', true);
44
+ switch (command) {
45
+ case 'capture':
46
+ return { kind: 'capture', options: parseCaptureArgs(rest) };
47
+ case 'render':
48
+ return { kind: 'render', options: parseRenderArgs(rest) };
49
+ default:
50
+ throw createUnknownSubcommandError(command);
99
51
  }
100
- const urls = parsed.positionals.map((raw) => parseWithCliError(raw, parseCaptureUrl));
101
- const durationOption = parsed.values.duration ?? DEFAULT_DURATION;
102
- if (durationOption !== 'auto' && Number.isNaN(Number(durationOption))) {
103
- throw new CliError(`--duration must be "auto" or a positive number (e.g. --duration 5): ${durationOption}`);
104
- }
105
- const outPath = resolveOutPath(parsed.values.out ?? DEFAULT_OUT_FILE);
106
- const pageGapSeconds = parseWithCliError(parsed.values['page-gap'] ?? '0', parseNonNegativeNumber);
107
- const hideSelectors = parsed.values['hide-selector'] ?? [];
108
- for (const selector of hideSelectors) {
109
- parseWithCliError(selector, validateHideSelector);
110
- }
111
- const fps = parseWithCliError(parsed.values.fps ?? String(DEFAULT_FPS), parsePositiveInt);
112
- if (fps > MAX_FPS) {
113
- throw new CliError(`--fps must be at most ${MAX_FPS}: ${fps}`);
114
- }
115
- return {
116
- urls: toNonEmptyArray(urls),
117
- outPath,
118
- manifestPath: parsed.values.manifest
119
- ? resolveOutPath(parsed.values.manifest)
120
- : deriveSidecarPath(outPath, '.manifest.json'),
121
- logFilePath: parsed.values['log-file']
122
- ? resolveOutPath(parsed.values['log-file'])
123
- : deriveSidecarPath(outPath, '.log.jsonl'),
124
- viewport: parseWithCliError(parsed.values.viewport ?? DEFAULT_VIEWPORT, parseViewport),
125
- fps,
126
- duration: durationOption === 'auto'
127
- ? 'auto'
128
- : parseWithCliError(durationOption, parsePositiveNumber),
129
- motion: parseWithCliError(parsed.values.motion ?? DEFAULT_MOTION, parseMotion),
130
- timeoutMs: parseWithCliError(parsed.values.timeout ?? String(DEFAULT_TIMEOUT_MS), parsePositiveInt),
131
- waitFor: parseWithCliError(parsed.values['wait-for'] ?? 'load', parseWaitFor),
132
- hideSelectors,
133
- pageGapSeconds,
134
- debugFramesDir: parsed.values['debug-frames-dir']
135
- ? resolveOutPath(parsed.values['debug-frames-dir'])
136
- : undefined,
137
- force: parsed.values.force === true,
138
- };
139
52
  }
140
- function toNonEmptyArray(items) {
141
- if (items.length === 0) {
142
- throw new CliError('capture requires at least one URL.', true);
143
- }
144
- return items;
53
+ export function parseCliArgs(argv = process.argv.slice(2)) {
54
+ const args = argv[0] === 'capture' ? argv.slice(1) : argv;
55
+ return parseCaptureArgs(args);
145
56
  }
146
57
  export function formatUsage() {
147
58
  return [
148
- `rollberry v${VERSION} — Capture smooth scroll videos from web pages`,
59
+ `rollberry v${VERSION} — Capture and render web pages into video`,
149
60
  '',
150
61
  'Usage:',
151
62
  ' rollberry capture <url...> [options]',
63
+ ' rollberry render <project.json> [options]',
152
64
  ' rollberry --help | -h',
153
65
  ' rollberry --version | -V',
154
66
  '',
155
- 'Options:',
67
+ 'Capture Options:',
156
68
  ' --out <file> Output MP4 path (default: ./rollberry.mp4)',
157
69
  ' --viewport <WxH> Viewport size (default: 1440x900)',
158
70
  ` --fps <n> Frames per second (default: 60, max: ${MAX_FPS})`,
@@ -167,13 +79,17 @@ export function formatUsage() {
167
79
  ' --manifest <file> Manifest JSON path (default: <out>.manifest.json)',
168
80
  ' --log-file <file> Log JSONL path (default: <out>.log.jsonl)',
169
81
  '',
82
+ 'Render Options:',
83
+ ' --output <name> Render only the named output (repeatable)',
84
+ ' --force Overwrite configured output files',
85
+ '',
170
86
  'Examples:',
171
87
  ' rollberry capture http://localhost:3000',
172
88
  ' rollberry capture https://example.com --out demo.mp4 --viewport 1920x1080',
173
- ' rollberry capture https://example.com --duration 10 --fps 30 --force',
89
+ ' rollberry render ./rollberry.project.json --output mobile',
174
90
  ].join('\n');
175
91
  }
176
- function parseWithCliError(rawValue, parser) {
92
+ export function parseWithCliError(rawValue, parser) {
177
93
  try {
178
94
  return parser(rawValue);
179
95
  }
@@ -184,17 +100,17 @@ function parseWithCliError(rawValue, parser) {
184
100
  throw new CliError(error instanceof Error ? error.message : 'Failed to parse arguments.');
185
101
  }
186
102
  }
187
- function resolveOutPath(path) {
103
+ export function resolveOutPath(path) {
188
104
  return resolve(process.cwd(), path);
189
105
  }
190
- function deriveSidecarPath(path, suffix) {
106
+ export function deriveSidecarPath(path, suffix) {
191
107
  const extension = /\.([^.]+)$/u.exec(path);
192
108
  if (!extension) {
193
109
  return `${path}${suffix}`;
194
110
  }
195
111
  return path.slice(0, -extension[0].length) + suffix;
196
112
  }
197
- function parseViewport(rawViewport) {
113
+ export function parseViewport(rawViewport) {
198
114
  const match = /^(?<width>\d+)x(?<height>\d+)$/u.exec(rawViewport);
199
115
  if (!match?.groups) {
200
116
  throw new Error(`--viewport must be in "WxH" format (e.g. "1440x900"): ${rawViewport}`);
@@ -206,34 +122,34 @@ function parseViewport(rawViewport) {
206
122
  }
207
123
  return { width, height };
208
124
  }
209
- function parsePositiveInt(rawValue) {
125
+ export function parsePositiveInt(rawValue) {
210
126
  const value = Number(rawValue);
211
127
  if (!Number.isInteger(value) || value <= 0) {
212
128
  throw new Error(`Expected a positive integer (e.g. "60"): ${rawValue}`);
213
129
  }
214
130
  return value;
215
131
  }
216
- function parsePositiveNumber(rawValue) {
132
+ export function parsePositiveNumber(rawValue) {
217
133
  const value = Number(rawValue);
218
134
  if (!Number.isFinite(value) || value <= 0) {
219
135
  throw new Error(`Expected a positive number (e.g. "5.0"): ${rawValue}`);
220
136
  }
221
137
  return value;
222
138
  }
223
- function parseNonNegativeNumber(rawValue) {
139
+ export function parseNonNegativeNumber(rawValue) {
224
140
  const value = Number(rawValue);
225
141
  if (!Number.isFinite(value) || value < 0) {
226
142
  throw new Error(`Expected a non-negative number (e.g. "0" or "1.5"): ${rawValue}`);
227
143
  }
228
144
  return value;
229
145
  }
230
- function parseMotion(rawMotion) {
146
+ export function parseMotion(rawMotion) {
231
147
  if (rawMotion === 'ease-in-out-sine' || rawMotion === 'linear') {
232
148
  return rawMotion;
233
149
  }
234
150
  throw new Error(`--motion must be "ease-in-out-sine" or "linear": ${rawMotion}`);
235
151
  }
236
- function parseWaitFor(rawWaitFor) {
152
+ export function parseWaitFor(rawWaitFor) {
237
153
  if (rawWaitFor === 'load') {
238
154
  return { kind: 'load' };
239
155
  }
@@ -255,3 +171,155 @@ function parseWaitFor(rawWaitFor) {
255
171
  }
256
172
  throw new Error(`--wait-for must be "load", "selector:<css>", or "ms:<n>": ${rawWaitFor}`);
257
173
  }
174
+ function parseCaptureArgs(args) {
175
+ if (args.includes('--help') || args.includes('-h')) {
176
+ throw new HelpRequest(formatUsage());
177
+ }
178
+ const parsed = parseArgs({
179
+ args,
180
+ allowPositionals: true,
181
+ options: {
182
+ out: {
183
+ type: 'string',
184
+ },
185
+ viewport: {
186
+ type: 'string',
187
+ },
188
+ fps: {
189
+ type: 'string',
190
+ },
191
+ duration: {
192
+ type: 'string',
193
+ },
194
+ motion: {
195
+ type: 'string',
196
+ },
197
+ timeout: {
198
+ type: 'string',
199
+ },
200
+ 'wait-for': {
201
+ type: 'string',
202
+ },
203
+ 'hide-selector': {
204
+ type: 'string',
205
+ multiple: true,
206
+ },
207
+ 'debug-frames-dir': {
208
+ type: 'string',
209
+ },
210
+ 'page-gap': {
211
+ type: 'string',
212
+ },
213
+ manifest: {
214
+ type: 'string',
215
+ },
216
+ 'log-file': {
217
+ type: 'string',
218
+ },
219
+ force: {
220
+ type: 'boolean',
221
+ },
222
+ },
223
+ strict: true,
224
+ });
225
+ if (parsed.positionals.length === 0) {
226
+ throw new CliError('capture requires at least one URL.', true);
227
+ }
228
+ const urls = parsed.positionals.map((raw) => parseWithCliError(raw, parseCaptureUrl));
229
+ const durationOption = parsed.values.duration ?? DEFAULT_DURATION;
230
+ if (durationOption !== 'auto' && Number.isNaN(Number(durationOption))) {
231
+ throw new CliError(`--duration must be "auto" or a positive number (e.g. --duration 5): ${durationOption}`);
232
+ }
233
+ const outPath = resolveOutPath(parsed.values.out ?? DEFAULT_OUT_FILE);
234
+ const pageGapSeconds = parseWithCliError(parsed.values['page-gap'] ?? '0', parseNonNegativeNumber);
235
+ const hideSelectors = parsed.values['hide-selector'] ?? [];
236
+ for (const selector of hideSelectors) {
237
+ parseWithCliError(selector, validateHideSelector);
238
+ }
239
+ const fps = parseWithCliError(parsed.values.fps ?? String(DEFAULT_FPS), parsePositiveInt);
240
+ if (fps > MAX_FPS) {
241
+ throw new CliError(`--fps must be at most ${MAX_FPS}: ${fps}`);
242
+ }
243
+ return {
244
+ urls: toNonEmptyArray(urls),
245
+ outPath,
246
+ manifestPath: parsed.values.manifest
247
+ ? resolveOutPath(parsed.values.manifest)
248
+ : deriveSidecarPath(outPath, '.manifest.json'),
249
+ logFilePath: parsed.values['log-file']
250
+ ? resolveOutPath(parsed.values['log-file'])
251
+ : deriveSidecarPath(outPath, '.log.jsonl'),
252
+ viewport: parseWithCliError(parsed.values.viewport ?? DEFAULT_VIEWPORT, parseViewport),
253
+ fps,
254
+ duration: durationOption === 'auto'
255
+ ? 'auto'
256
+ : parseWithCliError(durationOption, parsePositiveNumber),
257
+ motion: parseWithCliError(parsed.values.motion ?? DEFAULT_MOTION, parseMotion),
258
+ timeoutMs: parseWithCliError(parsed.values.timeout ?? String(DEFAULT_TIMEOUT_MS), parsePositiveInt),
259
+ waitFor: parseWithCliError(parsed.values['wait-for'] ?? 'load', parseWaitFor),
260
+ hideSelectors,
261
+ pageGapSeconds,
262
+ debugFramesDir: parsed.values['debug-frames-dir']
263
+ ? resolveOutPath(parsed.values['debug-frames-dir'])
264
+ : undefined,
265
+ force: parsed.values.force === true,
266
+ };
267
+ }
268
+ function parseRenderArgs(args) {
269
+ if (args.includes('--help') || args.includes('-h')) {
270
+ throw new HelpRequest(formatUsage());
271
+ }
272
+ const parsed = parseArgs({
273
+ args,
274
+ allowPositionals: true,
275
+ options: {
276
+ output: {
277
+ type: 'string',
278
+ multiple: true,
279
+ },
280
+ force: {
281
+ type: 'boolean',
282
+ },
283
+ },
284
+ strict: true,
285
+ });
286
+ if (parsed.positionals.length === 0) {
287
+ throw new CliError('render requires a project JSON path.', true);
288
+ }
289
+ if (parsed.positionals.length > 1) {
290
+ throw new CliError(`render accepts exactly one project JSON path: ${parsed.positionals.slice(1).join(', ')}`, true);
291
+ }
292
+ return {
293
+ projectPath: resolveOutPath(parsed.positionals[0]),
294
+ outputNames: parsed.values.output ?? [],
295
+ force: parsed.values.force === true,
296
+ };
297
+ }
298
+ function toNonEmptyArray(items) {
299
+ if (items.length === 0) {
300
+ throw new CliError('capture requires at least one URL.', true);
301
+ }
302
+ return items;
303
+ }
304
+ function createMissingSubcommandError() {
305
+ return new CliError([
306
+ 'A subcommand is required.',
307
+ '',
308
+ 'Available commands:',
309
+ ' capture Capture a scroll video from one or more URLs',
310
+ ' render Render a project JSON file into one or more videos',
311
+ '',
312
+ 'Run rollberry --help for more information.',
313
+ ].join('\n'));
314
+ }
315
+ function createUnknownSubcommandError(command) {
316
+ return new CliError([
317
+ `Unknown subcommand: ${command}`,
318
+ '',
319
+ 'Available commands:',
320
+ ' capture Capture a scroll video from one or more URLs',
321
+ ' render Render a project JSON file into one or more videos',
322
+ '',
323
+ 'Run rollberry --help for more information.',
324
+ ].join('\n'));
325
+ }
@@ -0,0 +1,27 @@
1
+ import type { CaptureAudioTrack, CaptureScene, CaptureSubtitleTrack, CaptureTransition, FinalVideoEncodingSettings, IntermediateArtifactProfile, NonEmptyArray, OutputFormat, Viewport } from './capture/types.js';
2
+ import { type RenderCliOptions } from './options.js';
3
+ export interface ResolvedRenderOutput {
4
+ name: string;
5
+ outPath: string;
6
+ format: OutputFormat;
7
+ manifestPath: string;
8
+ logFilePath: string;
9
+ viewport: Viewport;
10
+ fps: number;
11
+ force: boolean;
12
+ audio?: CaptureAudioTrack;
13
+ subtitles?: CaptureSubtitleTrack;
14
+ transition?: CaptureTransition;
15
+ intermediateArtifact: IntermediateArtifactProfile;
16
+ finalVideo: FinalVideoEncodingSettings;
17
+ debugFramesDir?: string;
18
+ }
19
+ export interface ResolvedRenderProject {
20
+ projectPath: string;
21
+ projectName: string;
22
+ summaryManifestPath: string;
23
+ timeoutMs: number;
24
+ scenes: NonEmptyArray<CaptureScene>;
25
+ outputs: NonEmptyArray<ResolvedRenderOutput>;
26
+ }
27
+ export declare function loadRenderProject(options: RenderCliOptions): Promise<ResolvedRenderProject>;