screenci 0.0.68 → 0.0.70
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 +45 -122
- package/dist/cli.d.ts +3 -18
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +117 -158
- package/dist/cli.js.map +1 -1
- package/dist/docs/manifest.d.ts +66 -35
- package/dist/docs/manifest.d.ts.map +1 -1
- package/dist/docs/manifest.js +22 -11
- package/dist/docs/manifest.js.map +1 -1
- package/dist/docs/video-sources/cli.screenci.d.ts +2 -0
- package/dist/docs/video-sources/cli.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/{cli.video.js → cli.screenci.js} +1 -1
- package/dist/docs/video-sources/cli.screenci.js.map +1 -0
- package/dist/docs/video-sources/installation.screenci.d.ts +2 -0
- package/dist/docs/video-sources/installation.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/{installation.video.js → installation.screenci.js} +1 -1
- package/dist/docs/video-sources/installation.screenci.js.map +1 -0
- package/dist/docs/video-sources/landing.screenci.d.ts +2 -0
- package/dist/docs/video-sources/landing.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/{landing.video.js → landing.screenci.js} +1 -1
- package/dist/docs/video-sources/landing.screenci.js.map +1 -0
- package/dist/docs/video-sources/locator-highlight-animated.screenci.d.ts +2 -0
- package/dist/docs/video-sources/locator-highlight-animated.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/locator-highlight-animated.screenci.js +53 -0
- package/dist/docs/video-sources/locator-highlight-animated.screenci.js.map +1 -0
- package/dist/docs/video-sources/locator-highlight-still.screenci.d.ts +2 -0
- package/dist/docs/video-sources/locator-highlight-still.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/locator-highlight-still.screenci.js +24 -0
- package/dist/docs/video-sources/locator-highlight-still.screenci.js.map +1 -0
- package/dist/docs/video-sources/product-pitch.screenci.d.ts +2 -0
- package/dist/docs/video-sources/product-pitch.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/product-pitch.screenci.js +136 -0
- package/dist/docs/video-sources/product-pitch.screenci.js.map +1 -0
- package/dist/docs/video-sources/public-urls.screenci.d.ts +2 -0
- package/dist/docs/video-sources/public-urls.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/public-urls.screenci.js +34 -0
- package/dist/docs/video-sources/public-urls.screenci.js.map +1 -0
- package/dist/docs/video-sources/studio.screenci.d.ts +2 -0
- package/dist/docs/video-sources/studio.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/studio.screenci.js +57 -0
- package/dist/docs/video-sources/studio.screenci.js.map +1 -0
- package/dist/docs/videos.d.ts +36 -15
- package/dist/docs/videos.d.ts.map +1 -1
- package/dist/docs/videos.js +27 -2
- package/dist/docs/videos.js.map +1 -1
- package/dist/e2e/htmlRasterizer.e2e.d.ts +2 -0
- package/dist/e2e/htmlRasterizer.e2e.d.ts.map +1 -0
- package/dist/e2e/htmlRasterizer.e2e.js +107 -0
- package/dist/e2e/htmlRasterizer.e2e.js.map +1 -0
- package/dist/e2e/screenshot.e2e.d.ts +2 -0
- package/dist/e2e/screenshot.e2e.d.ts.map +1 -0
- package/dist/e2e/screenshot.e2e.js +73 -0
- package/dist/e2e/screenshot.e2e.js.map +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/src/asset.d.ts +245 -53
- package/dist/src/asset.d.ts.map +1 -1
- package/dist/src/asset.js +718 -77
- package/dist/src/asset.js.map +1 -1
- package/dist/src/audio.d.ts +127 -0
- package/dist/src/audio.d.ts.map +1 -0
- package/dist/src/audio.js +207 -0
- package/dist/src/audio.js.map +1 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +9 -3
- package/dist/src/config.js.map +1 -1
- package/dist/src/contextOptions.d.ts +48 -0
- package/dist/src/contextOptions.d.ts.map +1 -0
- package/dist/src/contextOptions.js +71 -0
- package/dist/src/contextOptions.js.map +1 -0
- package/dist/src/crop.d.ts +93 -0
- package/dist/src/crop.d.ts.map +1 -0
- package/dist/src/crop.js +133 -0
- package/dist/src/crop.js.map +1 -0
- package/dist/src/cue.d.ts +17 -4
- package/dist/src/cue.d.ts.map +1 -1
- package/dist/src/cue.js +31 -3
- package/dist/src/cue.js.map +1 -1
- package/dist/src/defaults.d.ts +13 -1
- package/dist/src/defaults.d.ts.map +1 -1
- package/dist/src/defaults.js +13 -0
- package/dist/src/defaults.js.map +1 -1
- package/dist/src/dimensions.d.ts +6 -0
- package/dist/src/dimensions.d.ts.map +1 -1
- package/dist/src/dimensions.js +15 -0
- package/dist/src/dimensions.js.map +1 -1
- package/dist/src/events.d.ts +283 -13
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +134 -5
- package/dist/src/events.js.map +1 -1
- package/dist/src/htmlRasterizer.d.ts +153 -0
- package/dist/src/htmlRasterizer.d.ts.map +1 -0
- package/dist/src/htmlRasterizer.js +386 -0
- package/dist/src/htmlRasterizer.js.map +1 -0
- package/dist/src/init.d.ts +17 -0
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +131 -11
- package/dist/src/init.js.map +1 -1
- package/dist/src/linkSession.d.ts +36 -0
- package/dist/src/linkSession.d.ts.map +1 -0
- package/dist/src/linkSession.js +122 -0
- package/dist/src/linkSession.js.map +1 -0
- package/dist/src/overlayFlush.d.ts +18 -0
- package/dist/src/overlayFlush.d.ts.map +1 -0
- package/dist/src/overlayFlush.js +56 -0
- package/dist/src/overlayFlush.js.map +1 -0
- package/dist/src/overlayRect.d.ts +69 -0
- package/dist/src/overlayRect.d.ts.map +1 -0
- package/dist/src/overlayRect.js +55 -0
- package/dist/src/overlayRect.js.map +1 -0
- package/dist/src/performance.d.ts +1 -3
- package/dist/src/performance.d.ts.map +1 -1
- package/dist/src/performance.js +0 -12
- package/dist/src/performance.js.map +1 -1
- package/dist/src/recording.d.ts +3 -2
- package/dist/src/recording.d.ts.map +1 -1
- package/dist/src/recordingData.d.ts +22 -2
- package/dist/src/recordingData.d.ts.map +1 -1
- package/dist/src/runtimeContext.d.ts +57 -1
- package/dist/src/runtimeContext.d.ts.map +1 -1
- package/dist/src/runtimeContext.js +40 -0
- package/dist/src/runtimeContext.js.map +1 -1
- package/dist/src/screenshot.d.ts +105 -0
- package/dist/src/screenshot.d.ts.map +1 -0
- package/dist/src/screenshot.js +188 -0
- package/dist/src/screenshot.js.map +1 -0
- package/dist/src/stillCapture.d.ts +68 -0
- package/dist/src/stillCapture.d.ts.map +1 -0
- package/dist/src/stillCapture.js +119 -0
- package/dist/src/stillCapture.js.map +1 -0
- package/dist/src/studio.d.ts +8 -11
- package/dist/src/studio.d.ts.map +1 -1
- package/dist/src/studio.js +2 -25
- package/dist/src/studio.js.map +1 -1
- package/dist/src/types.d.ts +125 -15
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/video.d.ts +54 -1
- package/dist/src/video.d.ts.map +1 -1
- package/dist/src/video.js +190 -42
- package/dist/src/video.js.map +1 -1
- package/dist/src/voices.d.ts +22 -9
- package/dist/src/voices.d.ts.map +1 -1
- package/dist/src/voices.js +6 -24
- package/dist/src/voices.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -2
- package/skills/screenci/SKILL.md +37 -15
- package/skills/screenci/references/init.md +3 -1
- package/skills/screenci/references/record.md +8 -3
- package/dist/docs/video-sources/cli.video.d.ts +0 -2
- package/dist/docs/video-sources/cli.video.d.ts.map +0 -1
- package/dist/docs/video-sources/cli.video.js.map +0 -1
- package/dist/docs/video-sources/installation.video.d.ts +0 -2
- package/dist/docs/video-sources/installation.video.d.ts.map +0 -1
- package/dist/docs/video-sources/installation.video.js.map +0 -1
- package/dist/docs/video-sources/landing.video.d.ts +0 -2
- package/dist/docs/video-sources/landing.video.d.ts.map +0 -1
- package/dist/docs/video-sources/landing.video.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -13,33 +13,26 @@ import { determinePackageManager, parsePackageManager, runInit, } from './src/in
|
|
|
13
13
|
import { SCREENCI_DISABLE_RECORDING_TIMINGS_ENV, SCREENCI_MOCK_RECORD_ENV, } from './src/runtimeMode.js';
|
|
14
14
|
import { DEFAULT_RECORD_UPLOAD_POLICY } from './src/defaults.js';
|
|
15
15
|
import { findDuplicateTitles, formatDuplicateTitlesMessage, } from './src/titleValidation.js';
|
|
16
|
+
import { createLinkSessionSpec, deletePersistedLinkSessionSpec, getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl, getLinkSessionFilePath, getScreenCIEnvironment, isStoredLinkSessionReusable, readPersistedLinkSessionSpec, SCREENCI_LINK_SESSION_FILE, writePersistedLinkSessionSpec, } from './src/linkSession.js';
|
|
17
|
+
// Re-export the environment-aware URL helpers so existing importers (and tests)
|
|
18
|
+
// can keep importing them from the CLI entrypoint.
|
|
19
|
+
export { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl };
|
|
16
20
|
const SCREENCI_MOCK_RECORD_DOCS_URL = 'https://screenci.com/docs/reference/cli/#--mock-record';
|
|
17
21
|
const SCREENCI_RECORD_DOCS_URL = 'https://screenci.com/docs/reference/cli/#screenci-record';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const SCREENCI_PRODUCTION_FRONTEND_URL = 'https://app.screenci.com';
|
|
22
|
-
const SCREENCI_DEVELOPMENT_BACKEND_URL = 'https://dev.api.screenci.com';
|
|
23
|
-
const SCREENCI_DEVELOPMENT_FRONTEND_URL = 'https://dev.app.screenci.com';
|
|
24
|
-
const SCREENCI_LINK_SESSION_FILE = 'link-session.json';
|
|
22
|
+
// Records the recordId of the most recent `screenci record` upload so
|
|
23
|
+
// `screenci info` can report exactly the run that was just made.
|
|
24
|
+
const SCREENCI_LAST_RECORD_FILE = 'last-record.json';
|
|
25
25
|
const SCREENCI_LINK_SESSION_POLL_INTERVAL_MS = 2_000;
|
|
26
26
|
// `record --poll` keeps a non-interactive session (an agent or CI) waiting for
|
|
27
27
|
// sign-in. We poll on a slower cadence than the interactive loop so a long wait
|
|
28
28
|
// for a human to click the link does not hammer the backend.
|
|
29
29
|
const SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS = 5_000;
|
|
30
|
+
// `record --poll-auth` does not wait forever: after this long without a
|
|
31
|
+
// completed sign-in we stop polling and exit cleanly so an agent or CI step does
|
|
32
|
+
// not hang indefinitely. The link stays valid, so the command can be rerun. The
|
|
33
|
+
// default can be overridden with SCREENCI_POLL_AUTH_TIMEOUT_MS (milliseconds).
|
|
34
|
+
const SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
30
35
|
const require = createRequire(import.meta.url);
|
|
31
|
-
function parseScreenCIEnvironment(value) {
|
|
32
|
-
if (value === undefined)
|
|
33
|
-
return undefined;
|
|
34
|
-
if (SCREENCI_ENVIRONMENT_OPTION_VALUES.includes(value)) {
|
|
35
|
-
return value;
|
|
36
|
-
}
|
|
37
|
-
throw new Error(`Invalid ${SCREENCI_ENVIRONMENT_VARIABLE} "${value}". Expected one of: ${SCREENCI_ENVIRONMENT_OPTION_VALUES.join(', ')}`);
|
|
38
|
-
}
|
|
39
|
-
function getScreenCIEnvironment() {
|
|
40
|
-
const parsed = parseScreenCIEnvironment(process.env[SCREENCI_ENVIRONMENT_VARIABLE]);
|
|
41
|
-
return parsed ?? 'prod';
|
|
42
|
-
}
|
|
43
36
|
/**
|
|
44
37
|
* Reports whether the current session can complete an interactive browser
|
|
45
38
|
* sign-in. A session is interactive only when both stdin and stdout are
|
|
@@ -221,25 +214,6 @@ function contentTypeForPath(filePath) {
|
|
|
221
214
|
};
|
|
222
215
|
return contentTypeMap[ext] ?? 'application/octet-stream';
|
|
223
216
|
}
|
|
224
|
-
/**
|
|
225
|
-
* One-line summary of Studio overrides for CLI output, e.g.
|
|
226
|
-
* `recording.size (1 → 0.8), narration "intro" (en)`.
|
|
227
|
-
*/
|
|
228
|
-
export function formatStudioChangeSummary(changes) {
|
|
229
|
-
return changes
|
|
230
|
-
.map((change) => {
|
|
231
|
-
const label = change.label ?? 'selection';
|
|
232
|
-
if (change.kind === 'narration') {
|
|
233
|
-
return change.language !== undefined
|
|
234
|
-
? `${label} (${change.language})`
|
|
235
|
-
: label;
|
|
236
|
-
}
|
|
237
|
-
return change.from !== undefined && change.to !== undefined
|
|
238
|
-
? `${label} (${change.from} → ${change.to})`
|
|
239
|
-
: label;
|
|
240
|
-
})
|
|
241
|
-
.join(', ');
|
|
242
|
-
}
|
|
243
217
|
export function formatStudioUrl(appUrl, projectId, videoId) {
|
|
244
218
|
return `${appUrl}/project/${projectId}/video/${videoId}/studio`;
|
|
245
219
|
}
|
|
@@ -368,7 +342,15 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
368
342
|
let plan = null;
|
|
369
343
|
try {
|
|
370
344
|
uploadAbort.throwIfAborted();
|
|
371
|
-
|
|
345
|
+
// A screenshot recording uploads its raw page capture (always a PNG) through
|
|
346
|
+
// the same recording endpoint a video uses; the renderer reads those bytes as
|
|
347
|
+
// the capture. Videos upload recording.mp4. Output kind defaults to 'video'.
|
|
348
|
+
const isScreenshot = data.output === 'screenshot';
|
|
349
|
+
const recordingFileName = isScreenshot
|
|
350
|
+
? (data.screenshot?.path ?? 'screenshot.png')
|
|
351
|
+
: 'recording.mp4';
|
|
352
|
+
const recordingContentType = isScreenshot ? 'image/png' : 'video/mp4';
|
|
353
|
+
const recordingPath = resolve(screenciDir, entry, recordingFileName);
|
|
372
354
|
if (!existsSync(recordingPath)) {
|
|
373
355
|
progressReporter.complete(progressIndex, 'failure');
|
|
374
356
|
return {
|
|
@@ -376,7 +358,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
376
358
|
videoId: null,
|
|
377
359
|
hadFailure: true,
|
|
378
360
|
videoName,
|
|
379
|
-
failureMessage: `Missing
|
|
361
|
+
failureMessage: `Missing ${recordingFileName} for "${videoName}"`,
|
|
380
362
|
recordId,
|
|
381
363
|
};
|
|
382
364
|
}
|
|
@@ -434,7 +416,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
434
416
|
uploadAbort.throwIfAborted();
|
|
435
417
|
const fileStat = await stat(recordingPath);
|
|
436
418
|
if (verbose) {
|
|
437
|
-
logger.info(`Uploading
|
|
419
|
+
logger.info(`Uploading ${recordingFileName} size=${(fileStat.size / 1024 / 1024).toFixed(1)}MB`);
|
|
438
420
|
}
|
|
439
421
|
const recordingResponse = await withUploadRetry(async () => {
|
|
440
422
|
const stream = createReadStream(recordingPath);
|
|
@@ -446,7 +428,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
446
428
|
return await fetch(`${apiUrl}/cli/upload/${recordingId}/recording`, {
|
|
447
429
|
method: 'PUT',
|
|
448
430
|
headers: {
|
|
449
|
-
'Content-Type':
|
|
431
|
+
'Content-Type': recordingContentType,
|
|
450
432
|
'Content-Length': String(fileStat.size),
|
|
451
433
|
'X-ScreenCI-Secret': secret,
|
|
452
434
|
...(elevenLabsApiKey
|
|
@@ -1201,43 +1183,6 @@ async function countCompletedRecordings(screenciDir) {
|
|
|
1201
1183
|
}
|
|
1202
1184
|
return entries.filter((entry) => existsSync(resolve(screenciDir, entry, 'data.json'))).length;
|
|
1203
1185
|
}
|
|
1204
|
-
export function getDevBackendUrl() {
|
|
1205
|
-
switch (getScreenCIEnvironment()) {
|
|
1206
|
-
case 'local': {
|
|
1207
|
-
const devBackendPort = process.env.DEV_BACKEND_PORT;
|
|
1208
|
-
return devBackendPort
|
|
1209
|
-
? `http://localhost:${devBackendPort}`
|
|
1210
|
-
: 'http://localhost:8787';
|
|
1211
|
-
}
|
|
1212
|
-
case 'dev':
|
|
1213
|
-
return SCREENCI_DEVELOPMENT_BACKEND_URL;
|
|
1214
|
-
case 'prod':
|
|
1215
|
-
return SCREENCI_PRODUCTION_BACKEND_URL;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
export function getDevFrontendUrl() {
|
|
1219
|
-
switch (getScreenCIEnvironment()) {
|
|
1220
|
-
case 'local': {
|
|
1221
|
-
const devFrontendPort = process.env.DEV_FRONTEND_PORT;
|
|
1222
|
-
return devFrontendPort
|
|
1223
|
-
? `http://localhost:${devFrontendPort}`
|
|
1224
|
-
: 'http://localhost:5173';
|
|
1225
|
-
}
|
|
1226
|
-
case 'dev':
|
|
1227
|
-
return SCREENCI_DEVELOPMENT_FRONTEND_URL;
|
|
1228
|
-
case 'prod':
|
|
1229
|
-
return SCREENCI_PRODUCTION_FRONTEND_URL;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
export function getCliLinkSessionApiUrl() {
|
|
1233
|
-
// The `/cli-link/session` routes are Convex HTTP actions served from the same
|
|
1234
|
-
// backend host as the `/cli/*` upload endpoints. We hit that host directly
|
|
1235
|
-
// rather than the frontend: the frontend only forwards these routes via the
|
|
1236
|
-
// vite dev-server proxy, which does not exist on the hosted dev/prod frontend
|
|
1237
|
-
// (a POST there returns 405). The CLI runs under Node, so there is no CORS
|
|
1238
|
-
// constraint that would require going through the frontend origin.
|
|
1239
|
-
return getDevBackendUrl();
|
|
1240
|
-
}
|
|
1241
1186
|
function getScreenCISecretsUrl() {
|
|
1242
1187
|
return `${getDevFrontendUrl()}/secrets`;
|
|
1243
1188
|
}
|
|
@@ -1463,25 +1408,6 @@ async function requireScreenCISecret(configPath, opts = {}) {
|
|
|
1463
1408
|
apiUrl: getDevBackendUrl(),
|
|
1464
1409
|
};
|
|
1465
1410
|
}
|
|
1466
|
-
async function fetchProjectInfo(configPath) {
|
|
1467
|
-
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath, { interactive: detectInteractiveSession() });
|
|
1468
|
-
const url = new URL(`${apiUrl}/cli/project-info`);
|
|
1469
|
-
url.searchParams.set('projectName', screenciConfig.projectName);
|
|
1470
|
-
const res = await fetch(url.toString(), {
|
|
1471
|
-
headers: {
|
|
1472
|
-
'X-ScreenCI-Secret': secret,
|
|
1473
|
-
},
|
|
1474
|
-
});
|
|
1475
|
-
if (!res.ok) {
|
|
1476
|
-
const text = await res.text();
|
|
1477
|
-
throw new Error(`Failed to fetch project info: ${res.status} ${text}${hint401(res.status, secret)}`);
|
|
1478
|
-
}
|
|
1479
|
-
return (await res.json());
|
|
1480
|
-
}
|
|
1481
|
-
async function printProjectInfo(configPath) {
|
|
1482
|
-
const info = await fetchProjectInfo(configPath);
|
|
1483
|
-
process.stdout.write(`${JSON.stringify(info, null, 2)}\n`);
|
|
1484
|
-
}
|
|
1485
1411
|
async function updateVideoVisibility(videoId, isPublic, configPath) {
|
|
1486
1412
|
const { secret, apiUrl } = await requireScreenCISecret(configPath, {
|
|
1487
1413
|
interactive: detectInteractiveSession(),
|
|
@@ -1499,6 +1425,62 @@ async function updateVideoVisibility(videoId, isPublic, configPath) {
|
|
|
1499
1425
|
}
|
|
1500
1426
|
logger.info(`${isPublic ? 'Made public' : 'Made private'}: ${videoId}`);
|
|
1501
1427
|
}
|
|
1428
|
+
function getLastRecordFilePath(screenciDir) {
|
|
1429
|
+
return resolve(screenciDir, SCREENCI_LAST_RECORD_FILE);
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Persists the recordId of the just-completed `screenci record` upload so a
|
|
1433
|
+
* later `screenci info` can report exactly that run. Best-effort: a
|
|
1434
|
+
* failure to write must not fail the record command.
|
|
1435
|
+
*/
|
|
1436
|
+
async function saveLastRecordId(screenciDir, recordId) {
|
|
1437
|
+
try {
|
|
1438
|
+
mkdirSync(screenciDir, { recursive: true });
|
|
1439
|
+
await writeFile(getLastRecordFilePath(screenciDir), `${JSON.stringify({ recordId, savedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
1440
|
+
}
|
|
1441
|
+
catch (err) {
|
|
1442
|
+
logger.warn(`Failed to record run id for info: ${err instanceof Error ? err.message : String(err)}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
async function readLastRecordId(screenciDir) {
|
|
1446
|
+
try {
|
|
1447
|
+
const raw = await readFile(getLastRecordFilePath(screenciDir), 'utf-8');
|
|
1448
|
+
const parsed = JSON.parse(raw);
|
|
1449
|
+
return typeof parsed.recordId === 'string' ? parsed.recordId : null;
|
|
1450
|
+
}
|
|
1451
|
+
catch (err) {
|
|
1452
|
+
if (!isMissingFileError(err)) {
|
|
1453
|
+
logger.warn(`Ignoring invalid stored record at ${getLastRecordFilePath(screenciDir)}.`);
|
|
1454
|
+
}
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
// `screenci info` prints every project video and its public URLs as JSON. When
|
|
1459
|
+
// this machine has recorded a run (a recordId is stored in
|
|
1460
|
+
// .screenci/last-record.json), the backend also attaches, to the videos from
|
|
1461
|
+
// that run, a per-language `latestRecord` with render status and record-pinned
|
|
1462
|
+
// URLs. Without a local run, only the project-wide listing with `static` URLs is
|
|
1463
|
+
// returned. The server does the merge; the CLI just passes the recordId.
|
|
1464
|
+
async function printInfo(configPath) {
|
|
1465
|
+
const { resolvedConfigPath, screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath, {
|
|
1466
|
+
interactive: detectInteractiveSession(),
|
|
1467
|
+
});
|
|
1468
|
+
const screenciDir = resolve(dirname(resolvedConfigPath), '.screenci');
|
|
1469
|
+
const recordId = await readLastRecordId(screenciDir);
|
|
1470
|
+
const url = new URL(`${apiUrl}/cli/info`);
|
|
1471
|
+
url.searchParams.set('projectName', screenciConfig.projectName);
|
|
1472
|
+
if (recordId)
|
|
1473
|
+
url.searchParams.set('record', recordId);
|
|
1474
|
+
const res = await fetch(url.toString(), {
|
|
1475
|
+
headers: { 'X-ScreenCI-Secret': secret },
|
|
1476
|
+
});
|
|
1477
|
+
if (!res.ok) {
|
|
1478
|
+
const text = await res.text();
|
|
1479
|
+
throw new Error(`Failed to fetch info: ${res.status} ${text}${hint401(res.status, secret)}`);
|
|
1480
|
+
}
|
|
1481
|
+
const data = await res.json();
|
|
1482
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
1483
|
+
}
|
|
1502
1484
|
async function persistScreenCISecret(envFilePath, secret) {
|
|
1503
1485
|
const nextLine = `SCREENCI_SECRET=${secret}`;
|
|
1504
1486
|
try {
|
|
@@ -1525,57 +1507,6 @@ async function persistScreenCISecret(envFilePath, secret) {
|
|
|
1525
1507
|
}
|
|
1526
1508
|
await writeFile(envFilePath, `${nextLine}\n`);
|
|
1527
1509
|
}
|
|
1528
|
-
function getLinkSessionFilePath(projectDir) {
|
|
1529
|
-
return resolve(projectDir, '.screenci', SCREENCI_LINK_SESSION_FILE);
|
|
1530
|
-
}
|
|
1531
|
-
async function readPersistedLinkSessionSpec(specPath) {
|
|
1532
|
-
try {
|
|
1533
|
-
const raw = await readFile(specPath, 'utf-8');
|
|
1534
|
-
return JSON.parse(raw);
|
|
1535
|
-
}
|
|
1536
|
-
catch (error) {
|
|
1537
|
-
if (!isMissingFileError(error)) {
|
|
1538
|
-
logger.warn(`Ignoring invalid stored link session at ${specPath}.`);
|
|
1539
|
-
rmSync(specPath, { force: true });
|
|
1540
|
-
}
|
|
1541
|
-
return null;
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
async function writePersistedLinkSessionSpec(specPath, spec) {
|
|
1545
|
-
mkdirSync(dirname(specPath), { recursive: true });
|
|
1546
|
-
await writeFile(specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1547
|
-
}
|
|
1548
|
-
function deletePersistedLinkSessionSpec(specPath) {
|
|
1549
|
-
rmSync(specPath, { force: true });
|
|
1550
|
-
}
|
|
1551
|
-
function isStoredLinkSessionReusable(spec, options) {
|
|
1552
|
-
return (spec.environment === options.environment &&
|
|
1553
|
-
spec.envFilePath === options.envFilePath &&
|
|
1554
|
-
spec.resolvedConfigPath === options.resolvedConfigPath &&
|
|
1555
|
-
spec.expiresAt > new Date().toISOString());
|
|
1556
|
-
}
|
|
1557
|
-
async function createLinkSessionSpec(options) {
|
|
1558
|
-
const response = await fetch(`${options.apiUrl}/cli-link/session`, {
|
|
1559
|
-
method: 'POST',
|
|
1560
|
-
});
|
|
1561
|
-
if (!response.ok) {
|
|
1562
|
-
const text = await response.text();
|
|
1563
|
-
throw new Error(`Failed to create link session: ${response.status} ${text}`);
|
|
1564
|
-
}
|
|
1565
|
-
const body = (await response.json());
|
|
1566
|
-
return {
|
|
1567
|
-
token: body.token,
|
|
1568
|
-
appUrl: `${options.appUrl}/cli-auth?session=${encodeURIComponent(body.token)}`,
|
|
1569
|
-
pollUrl: `${options.apiUrl}/cli-link/session?token=${encodeURIComponent(body.token)}`,
|
|
1570
|
-
createdAt: body.createdAt,
|
|
1571
|
-
expiresAt: body.expiresAt,
|
|
1572
|
-
environment: options.environment,
|
|
1573
|
-
...(options.resolvedConfigPath
|
|
1574
|
-
? { resolvedConfigPath: options.resolvedConfigPath }
|
|
1575
|
-
: {}),
|
|
1576
|
-
envFilePath: options.envFilePath,
|
|
1577
|
-
};
|
|
1578
|
-
}
|
|
1579
1510
|
async function pollLinkSessionOnce(spec) {
|
|
1580
1511
|
const response = await fetch(spec.pollUrl);
|
|
1581
1512
|
const body = (await response.json());
|
|
@@ -1588,7 +1519,16 @@ async function pollLinkSessionOnce(spec) {
|
|
|
1588
1519
|
}
|
|
1589
1520
|
return { status };
|
|
1590
1521
|
}
|
|
1591
|
-
|
|
1522
|
+
function getPollAuthTimeoutMs() {
|
|
1523
|
+
const raw = process.env.SCREENCI_POLL_AUTH_TIMEOUT_MS;
|
|
1524
|
+
if (raw === undefined)
|
|
1525
|
+
return SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS;
|
|
1526
|
+
const parsed = Number(raw);
|
|
1527
|
+
return Number.isFinite(parsed) && parsed >= 0
|
|
1528
|
+
? parsed
|
|
1529
|
+
: SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS;
|
|
1530
|
+
}
|
|
1531
|
+
async function pollLinkSession(spec, pollIntervalMs = SCREENCI_LINK_SESSION_POLL_INTERVAL_MS, deadlineEpochMs) {
|
|
1592
1532
|
for (;;) {
|
|
1593
1533
|
const result = await pollLinkSessionOnce(spec);
|
|
1594
1534
|
if (result.status === 'completed' && result.secret) {
|
|
@@ -1599,6 +1539,11 @@ async function pollLinkSession(spec, pollIntervalMs = SCREENCI_LINK_SESSION_POLL
|
|
|
1599
1539
|
result.status === 'invalid') {
|
|
1600
1540
|
return result;
|
|
1601
1541
|
}
|
|
1542
|
+
// Stop before sleeping again once the optional deadline has passed so
|
|
1543
|
+
// `--poll-auth` cannot block forever waiting for a human to sign in.
|
|
1544
|
+
if (deadlineEpochMs !== undefined && Date.now() >= deadlineEpochMs) {
|
|
1545
|
+
return { status: 'timed-out' };
|
|
1546
|
+
}
|
|
1602
1547
|
await new Promise((resolveDelay) => setTimeout(resolveDelay, pollIntervalMs));
|
|
1603
1548
|
}
|
|
1604
1549
|
}
|
|
@@ -1672,14 +1617,24 @@ export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
|
1672
1617
|
// a slow cadence until the human finishes signing in, and continue
|
|
1673
1618
|
// recording automatically once the secret lands. We re-print the link
|
|
1674
1619
|
// on every (re)created session so the latest valid link is always
|
|
1675
|
-
// visible, including after a stale session is recreated.
|
|
1620
|
+
// visible, including after a stale session is recreated. We do not wait
|
|
1621
|
+
// forever: after a default timeout we stop polling and exit cleanly so
|
|
1622
|
+
// an agent or CI step does not hang. The link stays valid for a rerun.
|
|
1623
|
+
const timeoutMs = getPollAuthTimeoutMs();
|
|
1624
|
+
const timeoutMinutes = Math.round(timeoutMs / 60_000);
|
|
1625
|
+
const deadlineEpochMs = Date.now() + timeoutMs;
|
|
1676
1626
|
for (;;) {
|
|
1677
1627
|
logger.info(`Sign-in required to record. Open this link to sign in:\n${pc.cyan(spec.appUrl)}\n` +
|
|
1678
|
-
`Waiting for sign-in (checking every 5 seconds). Recording continues automatically once you finish.`);
|
|
1679
|
-
const polled = await pollLinkSession(spec, SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS);
|
|
1628
|
+
`Waiting for sign-in (checking every 5 seconds, up to ${timeoutMinutes} minutes). Recording continues automatically once you finish.`);
|
|
1629
|
+
const polled = await pollLinkSession(spec, SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS, deadlineEpochMs);
|
|
1680
1630
|
if (polled.status === 'completed' && polled.secret) {
|
|
1681
1631
|
return await saveCompletedSecret(polled.secret);
|
|
1682
1632
|
}
|
|
1633
|
+
if (polled.status === 'timed-out' || Date.now() >= deadlineEpochMs) {
|
|
1634
|
+
logger.info(`Timed out after ${timeoutMinutes} minutes waiting for sign-in. The link is still valid:\n${pc.cyan(spec.appUrl)}\n` +
|
|
1635
|
+
`After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to continue, or ${pc.cyan(getSuggestedScreenciCommand('record', '--poll-auth'))} to wait again.`);
|
|
1636
|
+
return undefined;
|
|
1637
|
+
}
|
|
1683
1638
|
deletePersistedLinkSessionSpec(specPath);
|
|
1684
1639
|
spec = await ensureSpec();
|
|
1685
1640
|
}
|
|
@@ -1721,7 +1676,7 @@ export async function main() {
|
|
|
1721
1676
|
.command('record [playwrightArgs...]')
|
|
1722
1677
|
.description('Record videos using Playwright')
|
|
1723
1678
|
.option('-v, --verbose', 'verbose output')
|
|
1724
|
-
.option('--poll-auth', 'wait for sign-in to complete (polling every 5s) instead of exiting, then continue recording')
|
|
1679
|
+
.option('--poll-auth', 'wait for sign-in to complete (polling every 5s, up to 5 minutes) instead of exiting, then continue recording')
|
|
1725
1680
|
.allowUnknownOption(true)
|
|
1726
1681
|
.action(async () => {
|
|
1727
1682
|
const parsed = parseRecordCliArgs(getSubcommandArgv('record'));
|
|
@@ -1792,6 +1747,10 @@ export async function main() {
|
|
|
1792
1747
|
throw err;
|
|
1793
1748
|
}
|
|
1794
1749
|
const { projectId, recordId, hadFailures, failedVideoNames, failedVideoMessages, studioNotices, plan, } = uploadResult;
|
|
1750
|
+
// Remember this run so `screenci info` can report exactly it.
|
|
1751
|
+
if (recordId !== null) {
|
|
1752
|
+
await saveLastRecordId(screenciDir, recordId);
|
|
1753
|
+
}
|
|
1795
1754
|
if (recordId !== null && projectId !== null) {
|
|
1796
1755
|
const recordUrl = `${appUrl}/record/${recordId}`;
|
|
1797
1756
|
await writeGitHubProjectOutput(recordUrl);
|
|
@@ -1823,9 +1782,9 @@ export async function main() {
|
|
|
1823
1782
|
logger.info(pc.cyan(formatStudioUrl(appUrl, projectId, notice.videoId)));
|
|
1824
1783
|
}
|
|
1825
1784
|
}
|
|
1826
|
-
else if (notice.studio.
|
|
1785
|
+
else if (notice.studio.applied) {
|
|
1827
1786
|
logger.info('');
|
|
1828
|
-
logger.info(`
|
|
1787
|
+
logger.info(`Studio configuration applied for "${notice.videoName}".`);
|
|
1829
1788
|
}
|
|
1830
1789
|
}
|
|
1831
1790
|
if (hadFailures) {
|
|
@@ -1884,10 +1843,10 @@ export async function main() {
|
|
|
1884
1843
|
});
|
|
1885
1844
|
program
|
|
1886
1845
|
.command('info')
|
|
1887
|
-
.description(
|
|
1846
|
+
.description("Print the latest record run's video URLs and render status as JSON")
|
|
1888
1847
|
.option('-c, --config <path>', 'path to screenci.config.ts')
|
|
1889
1848
|
.action(async (options) => {
|
|
1890
|
-
await
|
|
1849
|
+
await printInfo(options['config']);
|
|
1891
1850
|
});
|
|
1892
1851
|
program
|
|
1893
1852
|
.command('make-public <id>')
|