autokap 1.3.5 → 1.3.7
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/browser.d.ts +3 -1
- package/dist/browser.js +6 -3
- package/dist/cli.js +107 -18
- package/dist/web-playwright-local.js +7 -4
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -110,7 +110,9 @@ export declare class Browser {
|
|
|
110
110
|
* Create a Browser dedicated to clip capture. Frames are pulled via CDP
|
|
111
111
|
* `Page.captureScreenshot` in a tight loop by `ClipCaptureLoop` — NOT via
|
|
112
112
|
* Playwright's built-in `recordVideo` (which plateaus at 27 FPS with ~12%
|
|
113
|
-
* duplicates due to the CDP screencast throttler
|
|
113
|
+
* duplicates due to the CDP screencast throttler, and on cloud Linux runs
|
|
114
|
+
* the WebM encoder on the compositor thread → frame drops to ~4 fps under
|
|
115
|
+
* software rasterization).
|
|
114
116
|
*
|
|
115
117
|
* Preserves the HiDPI rendering path (`--force-device-scale-factor` +
|
|
116
118
|
* `--window-size`) so the captured frames match viewport × DSF pixels, and
|
package/dist/browser.js
CHANGED
|
@@ -798,7 +798,9 @@ export class Browser {
|
|
|
798
798
|
* Create a Browser dedicated to clip capture. Frames are pulled via CDP
|
|
799
799
|
* `Page.captureScreenshot` in a tight loop by `ClipCaptureLoop` — NOT via
|
|
800
800
|
* Playwright's built-in `recordVideo` (which plateaus at 27 FPS with ~12%
|
|
801
|
-
* duplicates due to the CDP screencast throttler
|
|
801
|
+
* duplicates due to the CDP screencast throttler, and on cloud Linux runs
|
|
802
|
+
* the WebM encoder on the compositor thread → frame drops to ~4 fps under
|
|
803
|
+
* software rasterization).
|
|
802
804
|
*
|
|
803
805
|
* Preserves the HiDPI rendering path (`--force-device-scale-factor` +
|
|
804
806
|
* `--window-size`) so the captured frames match viewport × DSF pixels, and
|
|
@@ -832,13 +834,14 @@ export class Browser {
|
|
|
832
834
|
headless: !options.headed,
|
|
833
835
|
args: clipArgs,
|
|
834
836
|
});
|
|
835
|
-
|
|
837
|
+
const contextOptions = {
|
|
836
838
|
viewport: options.viewport,
|
|
837
839
|
deviceScaleFactor,
|
|
838
840
|
locale: langToLocale(options.lang ?? 'en'),
|
|
839
841
|
colorScheme: options.colorScheme ?? 'light',
|
|
840
842
|
storageState: options.storageState,
|
|
841
|
-
}
|
|
843
|
+
};
|
|
844
|
+
instance.context = await instance.browser.newContext(contextOptions);
|
|
842
845
|
// Inject cursor overlay at context level — survives all navigations in this session
|
|
843
846
|
await instance.context.addInitScript(cursorScript);
|
|
844
847
|
// Also hide dev/prototype chrome on every document, including navigations
|
package/dist/cli.js
CHANGED
|
@@ -79,6 +79,40 @@ function ensureExportFormat(format) {
|
|
|
79
79
|
function printJson(value) {
|
|
80
80
|
console.log(JSON.stringify(value, null, 2));
|
|
81
81
|
}
|
|
82
|
+
function displayPresetName(preset) {
|
|
83
|
+
const name = preset.name?.trim();
|
|
84
|
+
return name && name.length > 0 ? name : preset.id;
|
|
85
|
+
}
|
|
86
|
+
function cloudCaptureProgressCheckpoint(event) {
|
|
87
|
+
switch (event.type) {
|
|
88
|
+
case 'variant_start':
|
|
89
|
+
return {
|
|
90
|
+
type: 'preset_progress',
|
|
91
|
+
message: `Capture started: ${event.variantId}`,
|
|
92
|
+
};
|
|
93
|
+
case 'variant_end':
|
|
94
|
+
return {
|
|
95
|
+
type: 'preset_progress',
|
|
96
|
+
message: event.status === 'ok'
|
|
97
|
+
? `Capture completed: ${event.variantId}`
|
|
98
|
+
: `Capture failed: ${event.variantId}`,
|
|
99
|
+
status: event.status === 'failed' ? 'running' : undefined,
|
|
100
|
+
};
|
|
101
|
+
case 'upload_start':
|
|
102
|
+
return {
|
|
103
|
+
type: 'upload_start',
|
|
104
|
+
message: 'Export started',
|
|
105
|
+
};
|
|
106
|
+
case 'upload_end':
|
|
107
|
+
return {
|
|
108
|
+
type: 'upload_end',
|
|
109
|
+
message: event.status === 'failed' ? 'Export failed' : 'Export completed',
|
|
110
|
+
status: event.status === 'failed' ? 'running' : undefined,
|
|
111
|
+
};
|
|
112
|
+
default:
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
82
116
|
function buildEndpointAssetUrl(config, endpointId) {
|
|
83
117
|
return `${config.apiBaseUrl}/api/v1/assets/${endpointId}`;
|
|
84
118
|
}
|
|
@@ -282,19 +316,31 @@ program
|
|
|
282
316
|
return;
|
|
283
317
|
lastProgressCheckpointAt = now;
|
|
284
318
|
}
|
|
319
|
+
// Surface failures loudly to stderr (logger.error → fly.io machine logs).
|
|
320
|
+
// Cloud→cloud HTTP can fail silently for many reasons (DNS, firewall,
|
|
321
|
+
// expired token, dashboard cold-start) and dropping a single checkpoint
|
|
322
|
+
// produces a "stuck progress bar" symptom on the dashboard with no clue.
|
|
323
|
+
// The URL + status + error code are the minimum needed to debug from
|
|
324
|
+
// `flyctl logs` after the fact.
|
|
325
|
+
const checkpointType = typeof body.type === 'string' ? body.type : 'unknown';
|
|
285
326
|
try {
|
|
286
327
|
const response = await fetch(checkpointUrl, {
|
|
287
328
|
method: 'POST',
|
|
288
329
|
headers: { ...authHeaders(config), 'Content-Type': 'application/json' },
|
|
289
330
|
body: JSON.stringify(body),
|
|
331
|
+
signal: AbortSignal.timeout(15_000),
|
|
290
332
|
});
|
|
291
333
|
if (!response.ok) {
|
|
292
334
|
const bodyText = await response.text().catch(() => response.statusText);
|
|
293
|
-
logger.
|
|
335
|
+
logger.error(`[auto-recapture] Cloud checkpoint POST failed: status=${response.status} ` +
|
|
336
|
+
`type=${checkpointType} url=${checkpointUrl} body=${bodyText.slice(0, 200)}`);
|
|
294
337
|
}
|
|
295
338
|
}
|
|
296
339
|
catch (err) {
|
|
297
|
-
|
|
340
|
+
const error = err;
|
|
341
|
+
const code = error.code ?? error.name ?? 'unknown';
|
|
342
|
+
logger.error(`[auto-recapture] Cloud checkpoint POST errored: code=${code} ` +
|
|
343
|
+
`type=${checkpointType} url=${checkpointUrl} message=${error.message}`);
|
|
298
344
|
}
|
|
299
345
|
};
|
|
300
346
|
/**
|
|
@@ -323,13 +369,57 @@ program
|
|
|
323
369
|
logger.warn(`[auto-recapture] Cloud callback failed (best-effort): ${err.message}`);
|
|
324
370
|
}
|
|
325
371
|
};
|
|
326
|
-
|
|
372
|
+
// Emit a CLI-side "booted" checkpoint before any other network call.
|
|
373
|
+
// The orchestrator already wrote a `run_start` "cloud runner machine
|
|
374
|
+
// started" event when it submitted the Fly machine — that event proves
|
|
375
|
+
// the machine was *scheduled*, not that the container actually booted.
|
|
376
|
+
// Emitting this from inside the container (and BEFORE the presets fetch)
|
|
377
|
+
// is the dashboard's only proof the CLI is alive. If this never arrives,
|
|
378
|
+
// the issue is container boot or fly→dashboard connectivity, not the
|
|
379
|
+
// capture pipeline.
|
|
380
|
+
const cliVersionForCheckpoint = process.env.AUTOKAP_CLI_VERSION ?? version;
|
|
381
|
+
await postCloudCheckpoint({
|
|
382
|
+
type: 'run_start',
|
|
383
|
+
status: 'running',
|
|
384
|
+
message: `CLI booted on cloud runner (autokap@${cliVersionForCheckpoint}) — fetching plan`,
|
|
385
|
+
});
|
|
386
|
+
// Fetch the presets list with a hard timeout. Without this, a slow or
|
|
387
|
+
// unreachable dashboard would leave the CLI hanging forever — the
|
|
388
|
+
// dashboard would stay stuck at "machine started" with no error surfaced.
|
|
389
|
+
const presetsPath = `/api/cli/projects/${opts.project}/auto-recapture-presets`;
|
|
390
|
+
let data;
|
|
391
|
+
try {
|
|
392
|
+
const response = await fetch(buildApiUrl(config, presetsPath), {
|
|
393
|
+
headers: authHeaders(config),
|
|
394
|
+
signal: AbortSignal.timeout(30_000),
|
|
395
|
+
});
|
|
396
|
+
if (!response.ok) {
|
|
397
|
+
const errorBody = await readApiError(response);
|
|
398
|
+
throw new Error(`HTTP ${response.status}: ${errorBody}`);
|
|
399
|
+
}
|
|
400
|
+
data = await response.json();
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
const err = error;
|
|
404
|
+
const isTimeout = err.name === 'TimeoutError' || err.name === 'AbortError';
|
|
405
|
+
const reason = isTimeout
|
|
406
|
+
? `presets fetch timed out after 30s — dashboard unreachable or unresponsive`
|
|
407
|
+
: `presets fetch failed: ${err.code ? `${err.code} ` : ''}${err.message}`;
|
|
408
|
+
await postCloudCheckpoint({
|
|
409
|
+
type: 'error',
|
|
410
|
+
status: 'failed',
|
|
411
|
+
message: reason,
|
|
412
|
+
errorMessage: reason,
|
|
413
|
+
});
|
|
414
|
+
await notifyCloudCallback('failed', { totalPresets: 0, failedPresets: 0, errorMessage: reason });
|
|
415
|
+
fatal(`Failed to list auto-recapture presets: ${reason}`);
|
|
416
|
+
}
|
|
327
417
|
await postCloudCheckpoint({
|
|
328
418
|
type: 'run_plan',
|
|
329
419
|
totalPresets: data.presets.length,
|
|
330
420
|
completedPresets: 0,
|
|
331
421
|
failedPresets: 0,
|
|
332
|
-
message: `planned ${data.presets.length} preset(s)`,
|
|
422
|
+
message: `Run planned: ${data.presets.length} preset(s)`,
|
|
333
423
|
});
|
|
334
424
|
if (data.presets.length === 0) {
|
|
335
425
|
logger.info(`[auto-recapture] No presets enabled for project ${opts.project}`);
|
|
@@ -343,9 +433,10 @@ program
|
|
|
343
433
|
totalPresets: data.presets.length,
|
|
344
434
|
completedPresets: 0,
|
|
345
435
|
failedPresets: 0,
|
|
346
|
-
message: `
|
|
436
|
+
message: `Runner started: ${data.presets.length} preset(s) to capture`,
|
|
347
437
|
});
|
|
348
438
|
for (const [index, preset] of data.presets.entries()) {
|
|
439
|
+
const presetDisplayName = displayPresetName(preset);
|
|
349
440
|
const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
|
|
350
441
|
logger.info(`[auto-recapture] Running ${label}`);
|
|
351
442
|
await postCloudCheckpoint({
|
|
@@ -356,7 +447,7 @@ program
|
|
|
356
447
|
totalPresets: data.presets.length,
|
|
357
448
|
completedPresets: index,
|
|
358
449
|
failedPresets: failures.length,
|
|
359
|
-
message: `
|
|
450
|
+
message: `Preset started: ${presetDisplayName}`,
|
|
360
451
|
});
|
|
361
452
|
const result = await runCapture({
|
|
362
453
|
presetId: preset.id,
|
|
@@ -364,23 +455,21 @@ program
|
|
|
364
455
|
headed: opts.headed,
|
|
365
456
|
allowUploadFailure: opts.allowUploadFailure,
|
|
366
457
|
onProgress: (event) => {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
? 'upload_end'
|
|
371
|
-
: 'preset_progress';
|
|
458
|
+
const checkpoint = cloudCaptureProgressCheckpoint(event);
|
|
459
|
+
if (!checkpoint)
|
|
460
|
+
return;
|
|
372
461
|
void postCloudCheckpoint({
|
|
373
|
-
type:
|
|
462
|
+
type: checkpoint.type,
|
|
374
463
|
presetId: preset.id,
|
|
375
464
|
presetName: preset.name ?? null,
|
|
376
465
|
presetIndex: index,
|
|
377
466
|
totalPresets: data.presets.length,
|
|
378
467
|
completedPresets: index,
|
|
379
468
|
failedPresets: failures.length,
|
|
380
|
-
message:
|
|
469
|
+
message: checkpoint.message,
|
|
381
470
|
progressEvent: event,
|
|
382
|
-
status:
|
|
383
|
-
}
|
|
471
|
+
status: checkpoint.status,
|
|
472
|
+
});
|
|
384
473
|
},
|
|
385
474
|
});
|
|
386
475
|
const childRunId = result.runId;
|
|
@@ -399,7 +488,7 @@ program
|
|
|
399
488
|
childRunId,
|
|
400
489
|
status: 'failed',
|
|
401
490
|
errorMessage: error,
|
|
402
|
-
message: `failed ${
|
|
491
|
+
message: `Preset failed: ${presetDisplayName}`,
|
|
403
492
|
});
|
|
404
493
|
}
|
|
405
494
|
else {
|
|
@@ -413,7 +502,7 @@ program
|
|
|
413
502
|
failedPresets: failures.length,
|
|
414
503
|
childRunId,
|
|
415
504
|
status: 'completed',
|
|
416
|
-
message: `completed ${
|
|
505
|
+
message: `Preset completed: ${presetDisplayName}`,
|
|
417
506
|
});
|
|
418
507
|
}
|
|
419
508
|
}
|
|
@@ -445,7 +534,7 @@ program
|
|
|
445
534
|
completedPresets: data.presets.length,
|
|
446
535
|
failedPresets: 0,
|
|
447
536
|
status: 'completed',
|
|
448
|
-
message: '
|
|
537
|
+
message: 'Cloud recapture completed',
|
|
449
538
|
});
|
|
450
539
|
await notifyCloudCallback('completed', {
|
|
451
540
|
totalPresets: data.presets.length,
|
|
@@ -446,14 +446,15 @@ export class WebPlaywrightLocal {
|
|
|
446
446
|
throw new Error('recording was not started');
|
|
447
447
|
}
|
|
448
448
|
if (this.recording.finalized) {
|
|
449
|
-
const
|
|
450
|
-
const
|
|
449
|
+
const sourcePath = this.recording.sourcePath ?? this.recording.mp4Path;
|
|
450
|
+
const buffer = await fs.readFile(sourcePath);
|
|
451
|
+
const durationMs = this.recording.encodedDurationMs ?? await getMediaDurationMs(sourcePath);
|
|
451
452
|
this.recording.encodedDurationMs = durationMs;
|
|
452
453
|
return {
|
|
453
454
|
buffer,
|
|
454
455
|
durationMs,
|
|
455
|
-
mimeType: 'video/mp4',
|
|
456
|
-
trimStartMs: this.recording.result?.trimStartMs ?? 0,
|
|
456
|
+
mimeType: this.recording.sourceMimeType ?? 'video/mp4',
|
|
457
|
+
trimStartMs: this.recording.trimStartMs ?? this.recording.result?.trimStartMs ?? 0,
|
|
457
458
|
};
|
|
458
459
|
}
|
|
459
460
|
if (this.recordingNavWatcher) {
|
|
@@ -475,6 +476,8 @@ export class WebPlaywrightLocal {
|
|
|
475
476
|
});
|
|
476
477
|
this.recording.finalized = true;
|
|
477
478
|
this.recording.result = result;
|
|
479
|
+
this.recording.sourcePath = this.recording.mp4Path;
|
|
480
|
+
this.recording.sourceMimeType = 'video/mp4';
|
|
478
481
|
this.recording.encodedDurationMs = await getMediaDurationMs(this.recording.mp4Path);
|
|
479
482
|
this.clipCursor = null;
|
|
480
483
|
const buffer = await fs.readFile(this.recording.mp4Path);
|