screenci 0.0.68 → 0.0.69
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 +1 -1
- package/dist/cli.d.ts +3 -18
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +105 -154
- package/dist/cli.js.map +1 -1
- package/dist/docs/manifest.d.ts +13 -13
- package/dist/docs/manifest.js +4 -4
- package/dist/docs/manifest.js.map +1 -1
- package/dist/docs/video-sources/studio.video.d.ts +2 -0
- package/dist/docs/video-sources/studio.video.d.ts.map +1 -0
- package/dist/docs/video-sources/studio.video.js +57 -0
- package/dist/docs/video-sources/studio.video.js.map +1 -0
- 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/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/src/asset.d.ts +141 -53
- package/dist/src/asset.d.ts.map +1 -1
- package/dist/src/asset.js +533 -78
- package/dist/src/asset.js.map +1 -1
- package/dist/src/audio.d.ts +113 -0
- package/dist/src/audio.d.ts.map +1 -0
- package/dist/src/audio.js +204 -0
- package/dist/src/audio.js.map +1 -0
- package/dist/src/defaults.d.ts +7 -1
- package/dist/src/defaults.d.ts.map +1 -1
- package/dist/src/defaults.js +7 -0
- package/dist/src/defaults.js.map +1 -1
- package/dist/src/events.d.ts +134 -6
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +73 -2
- package/dist/src/events.js.map +1 -1
- package/dist/src/htmlRasterizer.d.ts +110 -0
- package/dist/src/htmlRasterizer.d.ts.map +1 -0
- package/dist/src/htmlRasterizer.js +364 -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 +127 -7
- 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/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 +1 -1
- package/dist/src/recording.d.ts.map +1 -1
- package/dist/src/recordingData.d.ts +2 -2
- package/dist/src/recordingData.d.ts.map +1 -1
- package/dist/src/runtimeContext.d.ts +38 -0
- package/dist/src/runtimeContext.d.ts.map +1 -1
- package/dist/src/runtimeContext.js +25 -0
- package/dist/src/runtimeContext.js.map +1 -1
- package/dist/src/types.d.ts +25 -7
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/video.d.ts +34 -1
- package/dist/src/video.d.ts.map +1 -1
- package/dist/src/video.js +120 -19
- 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 +27 -5
- package/skills/screenci/references/init.md +2 -0
- package/skills/screenci/references/record.md +6 -1
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ That is also the right place for BYOK-style secrets such as
|
|
|
149
149
|
| `autoZoom` | Smooth camera follow for an interaction block |
|
|
150
150
|
| `zoomTo` | Manual camera framing for a locator or point |
|
|
151
151
|
| `resetZoom` | Returns from manual framing to the full viewport |
|
|
152
|
-
| `
|
|
152
|
+
| `createOverlays` | Inserts timed media overlays into the recording timeline |
|
|
153
153
|
| `voices` | Available voice constants such as `voices.Ava` |
|
|
154
154
|
| `modelTypes` | Narration model constants such as `modelTypes.consistent` |
|
|
155
155
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RecordingCustomVoiceRef, RecordingData } from './src/events.js';
|
|
2
2
|
import type { VoiceKey } from './src/voices.js';
|
|
3
3
|
import type { RecordUploadPolicy } from './src/types.js';
|
|
4
|
+
import { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl } from './src/linkSession.js';
|
|
5
|
+
export { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl };
|
|
4
6
|
type PlaywrightListReportSuite = {
|
|
5
7
|
title?: string;
|
|
6
8
|
specs?: Array<{
|
|
@@ -31,29 +33,16 @@ type PreparedUploadAsset = {
|
|
|
31
33
|
fileBuffer?: Buffer;
|
|
32
34
|
contentType?: string;
|
|
33
35
|
};
|
|
34
|
-
export type StudioAppliedChange = {
|
|
35
|
-
kind?: string;
|
|
36
|
-
label?: string;
|
|
37
|
-
cue?: string;
|
|
38
|
-
language?: string;
|
|
39
|
-
from?: string;
|
|
40
|
-
to?: string;
|
|
41
|
-
};
|
|
42
36
|
export type UploadStudioInfo = {
|
|
43
37
|
held: true;
|
|
44
38
|
} | {
|
|
45
|
-
|
|
39
|
+
applied: true;
|
|
46
40
|
};
|
|
47
41
|
export type StudioUploadNotice = {
|
|
48
42
|
videoName: string;
|
|
49
43
|
videoId: string | null;
|
|
50
44
|
studio: UploadStudioInfo;
|
|
51
45
|
};
|
|
52
|
-
/**
|
|
53
|
-
* One-line summary of Studio overrides for CLI output, e.g.
|
|
54
|
-
* `recording.size (1 → 0.8), narration "intro" (en)`.
|
|
55
|
-
*/
|
|
56
|
-
export declare function formatStudioChangeSummary(changes: StudioAppliedChange[]): string;
|
|
57
46
|
export declare function formatStudioUrl(appUrl: string, projectId: string, videoId: string): string;
|
|
58
47
|
type OrgPlan = 'free' | 'starter' | 'business';
|
|
59
48
|
export declare function withUploadRetry<T>(fn: () => Promise<T>, signal: AbortSignal | undefined, maxAttempts?: number): Promise<T>;
|
|
@@ -76,9 +65,6 @@ export declare function uploadRecordings(screenciDir: string, projectName: strin
|
|
|
76
65
|
studioNotices: StudioUploadNotice[];
|
|
77
66
|
plan: OrgPlan | null;
|
|
78
67
|
}>;
|
|
79
|
-
export declare function getDevBackendUrl(): string;
|
|
80
|
-
export declare function getDevFrontendUrl(): string;
|
|
81
|
-
export declare function getCliLinkSessionApiUrl(): string;
|
|
82
68
|
export declare function extractConfigStringLiteral(configSource: string, property: 'projectName' | 'envFile'): string | undefined;
|
|
83
69
|
export declare function extractRecordUploadPolicyLiteral(configSource: string): RecordUploadPolicy | undefined;
|
|
84
70
|
export declare function extractMockRecordLiteral(configSource: string): boolean | undefined;
|
|
@@ -89,5 +75,4 @@ export declare function ensureScreenciSecret(resolvedConfigPath?: string, opts?:
|
|
|
89
75
|
}): Promise<string | undefined>;
|
|
90
76
|
export declare function main(): Promise<void>;
|
|
91
77
|
export declare function logCliError(error: unknown): void;
|
|
92
|
-
export {};
|
|
93
78
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EACV,uBAAuB,EACvB,aAAa,EAEd,MAAM,iBAAiB,CAAA;AAMxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAkB,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
|
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
|
}
|
|
@@ -1201,43 +1175,6 @@ async function countCompletedRecordings(screenciDir) {
|
|
|
1201
1175
|
}
|
|
1202
1176
|
return entries.filter((entry) => existsSync(resolve(screenciDir, entry, 'data.json'))).length;
|
|
1203
1177
|
}
|
|
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
1178
|
function getScreenCISecretsUrl() {
|
|
1242
1179
|
return `${getDevFrontendUrl()}/secrets`;
|
|
1243
1180
|
}
|
|
@@ -1463,25 +1400,6 @@ async function requireScreenCISecret(configPath, opts = {}) {
|
|
|
1463
1400
|
apiUrl: getDevBackendUrl(),
|
|
1464
1401
|
};
|
|
1465
1402
|
}
|
|
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
1403
|
async function updateVideoVisibility(videoId, isPublic, configPath) {
|
|
1486
1404
|
const { secret, apiUrl } = await requireScreenCISecret(configPath, {
|
|
1487
1405
|
interactive: detectInteractiveSession(),
|
|
@@ -1499,6 +1417,62 @@ async function updateVideoVisibility(videoId, isPublic, configPath) {
|
|
|
1499
1417
|
}
|
|
1500
1418
|
logger.info(`${isPublic ? 'Made public' : 'Made private'}: ${videoId}`);
|
|
1501
1419
|
}
|
|
1420
|
+
function getLastRecordFilePath(screenciDir) {
|
|
1421
|
+
return resolve(screenciDir, SCREENCI_LAST_RECORD_FILE);
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Persists the recordId of the just-completed `screenci record` upload so a
|
|
1425
|
+
* later `screenci info` can report exactly that run. Best-effort: a
|
|
1426
|
+
* failure to write must not fail the record command.
|
|
1427
|
+
*/
|
|
1428
|
+
async function saveLastRecordId(screenciDir, recordId) {
|
|
1429
|
+
try {
|
|
1430
|
+
mkdirSync(screenciDir, { recursive: true });
|
|
1431
|
+
await writeFile(getLastRecordFilePath(screenciDir), `${JSON.stringify({ recordId, savedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
1432
|
+
}
|
|
1433
|
+
catch (err) {
|
|
1434
|
+
logger.warn(`Failed to record run id for info: ${err instanceof Error ? err.message : String(err)}`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
async function readLastRecordId(screenciDir) {
|
|
1438
|
+
try {
|
|
1439
|
+
const raw = await readFile(getLastRecordFilePath(screenciDir), 'utf-8');
|
|
1440
|
+
const parsed = JSON.parse(raw);
|
|
1441
|
+
return typeof parsed.recordId === 'string' ? parsed.recordId : null;
|
|
1442
|
+
}
|
|
1443
|
+
catch (err) {
|
|
1444
|
+
if (!isMissingFileError(err)) {
|
|
1445
|
+
logger.warn(`Ignoring invalid stored record at ${getLastRecordFilePath(screenciDir)}.`);
|
|
1446
|
+
}
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
// `screenci info` prints every project video and its public URLs as JSON. When
|
|
1451
|
+
// this machine has recorded a run (a recordId is stored in
|
|
1452
|
+
// .screenci/last-record.json), the backend also attaches, to the videos from
|
|
1453
|
+
// that run, a per-language `latestRecord` with render status and record-pinned
|
|
1454
|
+
// URLs. Without a local run, only the project-wide listing with `static` URLs is
|
|
1455
|
+
// returned. The server does the merge; the CLI just passes the recordId.
|
|
1456
|
+
async function printInfo(configPath) {
|
|
1457
|
+
const { resolvedConfigPath, screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath, {
|
|
1458
|
+
interactive: detectInteractiveSession(),
|
|
1459
|
+
});
|
|
1460
|
+
const screenciDir = resolve(dirname(resolvedConfigPath), '.screenci');
|
|
1461
|
+
const recordId = await readLastRecordId(screenciDir);
|
|
1462
|
+
const url = new URL(`${apiUrl}/cli/info`);
|
|
1463
|
+
url.searchParams.set('projectName', screenciConfig.projectName);
|
|
1464
|
+
if (recordId)
|
|
1465
|
+
url.searchParams.set('record', recordId);
|
|
1466
|
+
const res = await fetch(url.toString(), {
|
|
1467
|
+
headers: { 'X-ScreenCI-Secret': secret },
|
|
1468
|
+
});
|
|
1469
|
+
if (!res.ok) {
|
|
1470
|
+
const text = await res.text();
|
|
1471
|
+
throw new Error(`Failed to fetch info: ${res.status} ${text}${hint401(res.status, secret)}`);
|
|
1472
|
+
}
|
|
1473
|
+
const data = await res.json();
|
|
1474
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
1475
|
+
}
|
|
1502
1476
|
async function persistScreenCISecret(envFilePath, secret) {
|
|
1503
1477
|
const nextLine = `SCREENCI_SECRET=${secret}`;
|
|
1504
1478
|
try {
|
|
@@ -1525,57 +1499,6 @@ async function persistScreenCISecret(envFilePath, secret) {
|
|
|
1525
1499
|
}
|
|
1526
1500
|
await writeFile(envFilePath, `${nextLine}\n`);
|
|
1527
1501
|
}
|
|
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
1502
|
async function pollLinkSessionOnce(spec) {
|
|
1580
1503
|
const response = await fetch(spec.pollUrl);
|
|
1581
1504
|
const body = (await response.json());
|
|
@@ -1588,7 +1511,16 @@ async function pollLinkSessionOnce(spec) {
|
|
|
1588
1511
|
}
|
|
1589
1512
|
return { status };
|
|
1590
1513
|
}
|
|
1591
|
-
|
|
1514
|
+
function getPollAuthTimeoutMs() {
|
|
1515
|
+
const raw = process.env.SCREENCI_POLL_AUTH_TIMEOUT_MS;
|
|
1516
|
+
if (raw === undefined)
|
|
1517
|
+
return SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS;
|
|
1518
|
+
const parsed = Number(raw);
|
|
1519
|
+
return Number.isFinite(parsed) && parsed >= 0
|
|
1520
|
+
? parsed
|
|
1521
|
+
: SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS;
|
|
1522
|
+
}
|
|
1523
|
+
async function pollLinkSession(spec, pollIntervalMs = SCREENCI_LINK_SESSION_POLL_INTERVAL_MS, deadlineEpochMs) {
|
|
1592
1524
|
for (;;) {
|
|
1593
1525
|
const result = await pollLinkSessionOnce(spec);
|
|
1594
1526
|
if (result.status === 'completed' && result.secret) {
|
|
@@ -1599,6 +1531,11 @@ async function pollLinkSession(spec, pollIntervalMs = SCREENCI_LINK_SESSION_POLL
|
|
|
1599
1531
|
result.status === 'invalid') {
|
|
1600
1532
|
return result;
|
|
1601
1533
|
}
|
|
1534
|
+
// Stop before sleeping again once the optional deadline has passed so
|
|
1535
|
+
// `--poll-auth` cannot block forever waiting for a human to sign in.
|
|
1536
|
+
if (deadlineEpochMs !== undefined && Date.now() >= deadlineEpochMs) {
|
|
1537
|
+
return { status: 'timed-out' };
|
|
1538
|
+
}
|
|
1602
1539
|
await new Promise((resolveDelay) => setTimeout(resolveDelay, pollIntervalMs));
|
|
1603
1540
|
}
|
|
1604
1541
|
}
|
|
@@ -1672,14 +1609,24 @@ export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
|
1672
1609
|
// a slow cadence until the human finishes signing in, and continue
|
|
1673
1610
|
// recording automatically once the secret lands. We re-print the link
|
|
1674
1611
|
// on every (re)created session so the latest valid link is always
|
|
1675
|
-
// visible, including after a stale session is recreated.
|
|
1612
|
+
// visible, including after a stale session is recreated. We do not wait
|
|
1613
|
+
// forever: after a default timeout we stop polling and exit cleanly so
|
|
1614
|
+
// an agent or CI step does not hang. The link stays valid for a rerun.
|
|
1615
|
+
const timeoutMs = getPollAuthTimeoutMs();
|
|
1616
|
+
const timeoutMinutes = Math.round(timeoutMs / 60_000);
|
|
1617
|
+
const deadlineEpochMs = Date.now() + timeoutMs;
|
|
1676
1618
|
for (;;) {
|
|
1677
1619
|
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);
|
|
1620
|
+
`Waiting for sign-in (checking every 5 seconds, up to ${timeoutMinutes} minutes). Recording continues automatically once you finish.`);
|
|
1621
|
+
const polled = await pollLinkSession(spec, SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS, deadlineEpochMs);
|
|
1680
1622
|
if (polled.status === 'completed' && polled.secret) {
|
|
1681
1623
|
return await saveCompletedSecret(polled.secret);
|
|
1682
1624
|
}
|
|
1625
|
+
if (polled.status === 'timed-out' || Date.now() >= deadlineEpochMs) {
|
|
1626
|
+
logger.info(`Timed out after ${timeoutMinutes} minutes waiting for sign-in. The link is still valid:\n${pc.cyan(spec.appUrl)}\n` +
|
|
1627
|
+
`After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to continue, or ${pc.cyan(getSuggestedScreenciCommand('record', '--poll-auth'))} to wait again.`);
|
|
1628
|
+
return undefined;
|
|
1629
|
+
}
|
|
1683
1630
|
deletePersistedLinkSessionSpec(specPath);
|
|
1684
1631
|
spec = await ensureSpec();
|
|
1685
1632
|
}
|
|
@@ -1721,7 +1668,7 @@ export async function main() {
|
|
|
1721
1668
|
.command('record [playwrightArgs...]')
|
|
1722
1669
|
.description('Record videos using Playwright')
|
|
1723
1670
|
.option('-v, --verbose', 'verbose output')
|
|
1724
|
-
.option('--poll-auth', 'wait for sign-in to complete (polling every 5s) instead of exiting, then continue recording')
|
|
1671
|
+
.option('--poll-auth', 'wait for sign-in to complete (polling every 5s, up to 5 minutes) instead of exiting, then continue recording')
|
|
1725
1672
|
.allowUnknownOption(true)
|
|
1726
1673
|
.action(async () => {
|
|
1727
1674
|
const parsed = parseRecordCliArgs(getSubcommandArgv('record'));
|
|
@@ -1792,6 +1739,10 @@ export async function main() {
|
|
|
1792
1739
|
throw err;
|
|
1793
1740
|
}
|
|
1794
1741
|
const { projectId, recordId, hadFailures, failedVideoNames, failedVideoMessages, studioNotices, plan, } = uploadResult;
|
|
1742
|
+
// Remember this run so `screenci info` can report exactly it.
|
|
1743
|
+
if (recordId !== null) {
|
|
1744
|
+
await saveLastRecordId(screenciDir, recordId);
|
|
1745
|
+
}
|
|
1795
1746
|
if (recordId !== null && projectId !== null) {
|
|
1796
1747
|
const recordUrl = `${appUrl}/record/${recordId}`;
|
|
1797
1748
|
await writeGitHubProjectOutput(recordUrl);
|
|
@@ -1823,9 +1774,9 @@ export async function main() {
|
|
|
1823
1774
|
logger.info(pc.cyan(formatStudioUrl(appUrl, projectId, notice.videoId)));
|
|
1824
1775
|
}
|
|
1825
1776
|
}
|
|
1826
|
-
else if (notice.studio.
|
|
1777
|
+
else if (notice.studio.applied) {
|
|
1827
1778
|
logger.info('');
|
|
1828
|
-
logger.info(`
|
|
1779
|
+
logger.info(`Studio configuration applied for "${notice.videoName}".`);
|
|
1829
1780
|
}
|
|
1830
1781
|
}
|
|
1831
1782
|
if (hadFailures) {
|
|
@@ -1884,10 +1835,10 @@ export async function main() {
|
|
|
1884
1835
|
});
|
|
1885
1836
|
program
|
|
1886
1837
|
.command('info')
|
|
1887
|
-
.description(
|
|
1838
|
+
.description("Print the latest record run's video URLs and render status as JSON")
|
|
1888
1839
|
.option('-c, --config <path>', 'path to screenci.config.ts')
|
|
1889
1840
|
.action(async (options) => {
|
|
1890
|
-
await
|
|
1841
|
+
await printInfo(options['config']);
|
|
1891
1842
|
});
|
|
1892
1843
|
program
|
|
1893
1844
|
.command('make-public <id>')
|