screenci 0.0.65 → 0.0.67
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/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +127 -62
- package/dist/cli.js.map +1 -1
- package/dist/src/changeFocus.d.ts.map +1 -1
- package/dist/src/changeFocus.js +66 -72
- package/dist/src/changeFocus.js.map +1 -1
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +4 -0
- package/dist/src/events.js.map +1 -1
- package/dist/src/mouse.d.ts +6 -0
- package/dist/src/mouse.d.ts.map +1 -1
- package/dist/src/mouse.js +15 -0
- package/dist/src/mouse.js.map +1 -1
- package/dist/src/types.d.ts +23 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +4 -0
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export type StudioUploadNotice = {
|
|
|
55
55
|
*/
|
|
56
56
|
export declare function formatStudioChangeSummary(changes: StudioAppliedChange[]): string;
|
|
57
57
|
export declare function formatStudioUrl(appUrl: string, projectId: string, videoId: string): string;
|
|
58
|
+
type OrgPlan = 'free' | 'starter' | 'business';
|
|
59
|
+
export declare function withUploadRetry<T>(fn: () => Promise<T>, signal: AbortSignal | undefined, maxAttempts?: number): Promise<T>;
|
|
58
60
|
export declare function attachUploadAbortStdinListener(input: Pick<NodeJS.ReadStream, 'on' | 'off' | 'pause'>, onAbort: (signal: NodeJS.Signals) => void): () => void;
|
|
59
61
|
export declare function collectUploadAssets(data: RecordingData, configDir: string): Promise<PreparedUploadAsset[]>;
|
|
60
62
|
export declare function stripVoicePath(voice: VoiceKey | RecordingCustomVoiceRef): VoiceKey | RecordingCustomVoiceRef;
|
|
@@ -72,6 +74,7 @@ export declare function uploadRecordings(screenciDir: string, projectName: strin
|
|
|
72
74
|
message: string;
|
|
73
75
|
}>;
|
|
74
76
|
studioNotices: StudioUploadNotice[];
|
|
77
|
+
plan: OrgPlan | null;
|
|
75
78
|
}>;
|
|
76
79
|
export declare function getDevBackendUrl(): string;
|
|
77
80
|
export declare function getDevFrontendUrl(): string;
|
|
@@ -82,6 +85,7 @@ export declare function extractMockRecordLiteral(configSource: string): boolean
|
|
|
82
85
|
export declare function getConfigModuleSpecifier(resolvedConfigPath: string): string;
|
|
83
86
|
export declare function ensureScreenciSecret(resolvedConfigPath?: string, opts?: {
|
|
84
87
|
interactive?: boolean;
|
|
88
|
+
pollAuth?: boolean;
|
|
85
89
|
}): Promise<string | undefined>;
|
|
86
90
|
export declare function main(): Promise<void>;
|
|
87
91
|
export declare function logCliError(error: unknown): void;
|
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
|
@@ -23,6 +23,10 @@ const SCREENCI_DEVELOPMENT_BACKEND_URL = 'https://dev.api.screenci.com';
|
|
|
23
23
|
const SCREENCI_DEVELOPMENT_FRONTEND_URL = 'https://dev.app.screenci.com';
|
|
24
24
|
const SCREENCI_LINK_SESSION_FILE = 'link-session.json';
|
|
25
25
|
const SCREENCI_LINK_SESSION_POLL_INTERVAL_MS = 2_000;
|
|
26
|
+
// `record --poll` keeps a non-interactive session (an agent or CI) waiting for
|
|
27
|
+
// sign-in. We poll on a slower cadence than the interactive loop so a long wait
|
|
28
|
+
// for a human to click the link does not hammer the backend.
|
|
29
|
+
const SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS = 5_000;
|
|
26
30
|
const require = createRequire(import.meta.url);
|
|
27
31
|
function parseScreenCIEnvironment(value) {
|
|
28
32
|
if (value === undefined)
|
|
@@ -88,13 +92,14 @@ function extractPlaywrightDiscoveryError(output) {
|
|
|
88
92
|
function logScreenCISecretGuide() {
|
|
89
93
|
logger.info(`Guide: ${pc.cyan(SCREENCI_RECORD_DOCS_URL)}`);
|
|
90
94
|
}
|
|
91
|
-
function getSuggestedScreenciCommand(command) {
|
|
95
|
+
function getSuggestedScreenciCommand(command, flags = '') {
|
|
96
|
+
const suffix = flags ? ` ${flags}` : '';
|
|
92
97
|
const pm = determinePackageManager();
|
|
93
98
|
if (pm === 'pnpm')
|
|
94
|
-
return `pnpm exec screenci ${command}`;
|
|
99
|
+
return `pnpm exec screenci ${command}${suffix}`;
|
|
95
100
|
if (pm === 'yarn')
|
|
96
|
-
return `yarn screenci ${command}`;
|
|
97
|
-
return `npx screenci ${command}`;
|
|
101
|
+
return `yarn screenci ${command}${suffix}`;
|
|
102
|
+
return `npx screenci ${command}${suffix}`;
|
|
98
103
|
}
|
|
99
104
|
async function collectDiscoveredTestTitles(configPath, additionalArgs, env) {
|
|
100
105
|
const listArgs = [
|
|
@@ -278,6 +283,20 @@ function isRecordFailureHintError(err) {
|
|
|
278
283
|
function isUploadAssetError(err) {
|
|
279
284
|
return err instanceof UploadAssetError;
|
|
280
285
|
}
|
|
286
|
+
export async function withUploadRetry(fn, signal, maxAttempts = 3) {
|
|
287
|
+
let lastErr;
|
|
288
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
289
|
+
try {
|
|
290
|
+
return await fn();
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
if (isUploadCancelledError(err))
|
|
294
|
+
throw err;
|
|
295
|
+
lastErr = err;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
throw lastErr;
|
|
299
|
+
}
|
|
281
300
|
function formatUploadProgressLine(videoName, status) {
|
|
282
301
|
switch (status) {
|
|
283
302
|
case undefined:
|
|
@@ -346,6 +365,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
346
365
|
const { entry, videoName, data, preparedUploadAssets } = candidate;
|
|
347
366
|
let projectId = null;
|
|
348
367
|
let videoId = null;
|
|
368
|
+
let plan = null;
|
|
349
369
|
try {
|
|
350
370
|
uploadAbort.throwIfAborted();
|
|
351
371
|
const recordingPath = resolve(screenciDir, entry, 'recording.mp4');
|
|
@@ -361,7 +381,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
361
381
|
};
|
|
362
382
|
}
|
|
363
383
|
const recordingHash = await hashFile(recordingPath);
|
|
364
|
-
const startResponse = await fetch(`${apiUrl}/cli/upload/start`, {
|
|
384
|
+
const startResponse = await withUploadRetry(() => fetch(`${apiUrl}/cli/upload/start`, {
|
|
365
385
|
method: 'POST',
|
|
366
386
|
headers: {
|
|
367
387
|
'Content-Type': 'application/json',
|
|
@@ -387,7 +407,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
387
407
|
})),
|
|
388
408
|
}),
|
|
389
409
|
signal: uploadAbort.signal,
|
|
390
|
-
});
|
|
410
|
+
}), uploadAbort.signal);
|
|
391
411
|
if (!startResponse.ok) {
|
|
392
412
|
const text = await startResponse.text();
|
|
393
413
|
progressReporter.complete(progressIndex, 'failure');
|
|
@@ -404,6 +424,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
404
424
|
const { recordingId } = startBody;
|
|
405
425
|
projectId = startBody.projectId;
|
|
406
426
|
videoId = startBody.videoId ?? null;
|
|
427
|
+
plan = startBody.plan ?? null;
|
|
407
428
|
const studio = startBody.studio;
|
|
408
429
|
if (verbose) {
|
|
409
430
|
logger.info(`recordingId=${recordingId} projectId=${projectId}`);
|
|
@@ -415,44 +436,45 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
415
436
|
if (verbose) {
|
|
416
437
|
logger.info(`Uploading recording.mp4 size=${(fileStat.size / 1024 / 1024).toFixed(1)}MB`);
|
|
417
438
|
}
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
once: true
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
});
|
|
441
|
-
if (!recordingResponse.ok) {
|
|
442
|
-
const text = await recordingResponse.text();
|
|
443
|
-
progressReporter.complete(progressIndex, 'failure');
|
|
444
|
-
return {
|
|
445
|
-
projectId,
|
|
446
|
-
videoId,
|
|
447
|
-
hadFailure: true,
|
|
448
|
-
videoName,
|
|
449
|
-
failureMessage: `Failed to upload recording for "${videoName}": ${recordingResponse.status} ${text}${hint401(recordingResponse.status, secret)}`,
|
|
450
|
-
recordId,
|
|
451
|
-
};
|
|
439
|
+
const recordingResponse = await withUploadRetry(async () => {
|
|
440
|
+
const stream = createReadStream(recordingPath);
|
|
441
|
+
const abortStream = () => {
|
|
442
|
+
stream.destroy(new UploadCancelledError(`Upload cancelled for "${videoName}"`));
|
|
443
|
+
};
|
|
444
|
+
uploadAbort.signal.addEventListener('abort', abortStream, { once: true });
|
|
445
|
+
try {
|
|
446
|
+
return await fetch(`${apiUrl}/cli/upload/${recordingId}/recording`, {
|
|
447
|
+
method: 'PUT',
|
|
448
|
+
headers: {
|
|
449
|
+
'Content-Type': 'video/mp4',
|
|
450
|
+
'Content-Length': String(fileStat.size),
|
|
451
|
+
'X-ScreenCI-Secret': secret,
|
|
452
|
+
...(elevenLabsApiKey
|
|
453
|
+
? { 'X-ElevenLabs-Api-Key': elevenLabsApiKey }
|
|
454
|
+
: {}),
|
|
455
|
+
},
|
|
456
|
+
body: stream,
|
|
457
|
+
signal: uploadAbort.signal,
|
|
458
|
+
// @ts-expect-error Node.js fetch supports duplex for streaming
|
|
459
|
+
duplex: 'half',
|
|
460
|
+
});
|
|
452
461
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
462
|
+
finally {
|
|
463
|
+
uploadAbort.signal.removeEventListener('abort', abortStream);
|
|
464
|
+
}
|
|
465
|
+
}, uploadAbort.signal);
|
|
466
|
+
if (!recordingResponse.ok) {
|
|
467
|
+
const text = await recordingResponse.text();
|
|
468
|
+
progressReporter.complete(progressIndex, 'failure');
|
|
469
|
+
return {
|
|
470
|
+
projectId,
|
|
471
|
+
videoId,
|
|
472
|
+
hadFailure: true,
|
|
473
|
+
videoName,
|
|
474
|
+
failureMessage: `Failed to upload recording for "${videoName}": ${recordingResponse.status} ${text}${hint401(recordingResponse.status, secret)}`,
|
|
475
|
+
recordId,
|
|
476
|
+
...(plan !== null && { plan }),
|
|
477
|
+
};
|
|
456
478
|
}
|
|
457
479
|
progressReporter.complete(progressIndex, 'success');
|
|
458
480
|
cleanupUploadedRecordingDir(screenciDir, entry);
|
|
@@ -463,6 +485,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
463
485
|
videoName,
|
|
464
486
|
recordId,
|
|
465
487
|
...(studio !== undefined && { studio }),
|
|
488
|
+
...(plan !== null && { plan }),
|
|
466
489
|
};
|
|
467
490
|
}
|
|
468
491
|
catch (err) {
|
|
@@ -479,6 +502,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
479
502
|
videoName,
|
|
480
503
|
failureMessage: err instanceof Error ? err.message : String(err),
|
|
481
504
|
recordId,
|
|
505
|
+
...(plan !== null && { plan }),
|
|
482
506
|
};
|
|
483
507
|
}
|
|
484
508
|
progressReporter.complete(progressIndex, 'failure');
|
|
@@ -489,6 +513,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
489
513
|
videoName,
|
|
490
514
|
failureMessage: `Network error uploading "${videoName}": ${err instanceof Error ? err.message : String(err)}`,
|
|
491
515
|
recordId,
|
|
516
|
+
...(plan !== null && { plan }),
|
|
492
517
|
};
|
|
493
518
|
}
|
|
494
519
|
}
|
|
@@ -1022,7 +1047,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1022
1047
|
for (const asset of assets) {
|
|
1023
1048
|
throwIfAborted();
|
|
1024
1049
|
try {
|
|
1025
|
-
const checkRes = await fetch(`${apiUrl}/cli/upload/${recordingId}/asset/check`, {
|
|
1050
|
+
const checkRes = await withUploadRetry(() => fetch(`${apiUrl}/cli/upload/${recordingId}/asset/check`, {
|
|
1026
1051
|
method: 'POST',
|
|
1027
1052
|
headers: {
|
|
1028
1053
|
'Content-Type': 'application/json',
|
|
@@ -1036,7 +1061,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1036
1061
|
...(typeof asset.name === 'string' ? { name: asset.name } : {}),
|
|
1037
1062
|
}),
|
|
1038
1063
|
signal,
|
|
1039
|
-
});
|
|
1064
|
+
}), signal);
|
|
1040
1065
|
if (!checkRes.ok) {
|
|
1041
1066
|
const text = await checkRes.text();
|
|
1042
1067
|
throw new UploadAssetError(`Failed to check asset ${asset.path}: ${checkRes.status} ${text}${hint401(checkRes.status, secret)}`);
|
|
@@ -1049,8 +1074,9 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1049
1074
|
if (!asset.fileBuffer || !asset.contentType) {
|
|
1050
1075
|
throw new UploadAssetError(`Asset bytes not available for upload and backend does not have it yet: ${asset.path}`);
|
|
1051
1076
|
}
|
|
1077
|
+
const fileBuffer = asset.fileBuffer;
|
|
1052
1078
|
throwIfAborted();
|
|
1053
|
-
const res = await fetch(`${apiUrl}/cli/upload/${recordingId}/asset`, {
|
|
1079
|
+
const res = await withUploadRetry(() => fetch(`${apiUrl}/cli/upload/${recordingId}/asset`, {
|
|
1054
1080
|
method: 'PUT',
|
|
1055
1081
|
headers: {
|
|
1056
1082
|
'Content-Type': 'application/json',
|
|
@@ -1058,14 +1084,14 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1058
1084
|
},
|
|
1059
1085
|
body: JSON.stringify({
|
|
1060
1086
|
fileHash: asset.fileHash,
|
|
1061
|
-
fileBase64:
|
|
1087
|
+
fileBase64: fileBuffer.toString('base64'),
|
|
1062
1088
|
contentType: asset.contentType,
|
|
1063
1089
|
size: asset.size,
|
|
1064
1090
|
path: asset.path,
|
|
1065
1091
|
...(typeof asset.name === 'string' ? { name: asset.name } : {}),
|
|
1066
1092
|
}),
|
|
1067
1093
|
signal,
|
|
1068
|
-
});
|
|
1094
|
+
}), signal);
|
|
1069
1095
|
if (!res.ok) {
|
|
1070
1096
|
const text = await res.text();
|
|
1071
1097
|
if (res.status === 409 && text.includes('already exists')) {
|
|
@@ -1106,6 +1132,7 @@ export async function uploadRecordings(screenciDir, projectName, apiUrl, secret,
|
|
|
1106
1132
|
failedVideoNames: [],
|
|
1107
1133
|
failedVideoMessages: [],
|
|
1108
1134
|
studioNotices: [],
|
|
1135
|
+
plan: null,
|
|
1109
1136
|
};
|
|
1110
1137
|
}
|
|
1111
1138
|
if (specificEntry !== undefined) {
|
|
@@ -1126,12 +1153,14 @@ export async function uploadRecordings(screenciDir, projectName, apiUrl, secret,
|
|
|
1126
1153
|
failedVideoNames: [],
|
|
1127
1154
|
failedVideoMessages: [],
|
|
1128
1155
|
studioNotices: [],
|
|
1156
|
+
plan: null,
|
|
1129
1157
|
};
|
|
1130
1158
|
}
|
|
1131
1159
|
const progressReporter = createUploadProgressReporter(candidates.map((candidate) => candidate.videoName), verbose);
|
|
1132
1160
|
const results = await Promise.all(candidates.map(async (candidate, index) => await uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl, secret, elevenLabsApiKey, verbose, uploadAbort, progressReporter, index, recordId)));
|
|
1133
1161
|
firstProjectId =
|
|
1134
1162
|
results.find((result) => result.projectId !== null)?.projectId ?? null;
|
|
1163
|
+
const resolvedPlan = results.find((result) => result.plan !== undefined)?.plan ?? null;
|
|
1135
1164
|
const hadFailures = results.some((result) => result.hadFailure);
|
|
1136
1165
|
const failedVideoNames = results
|
|
1137
1166
|
.filter((result) => result.hadFailure)
|
|
@@ -1155,6 +1184,7 @@ export async function uploadRecordings(screenciDir, projectName, apiUrl, secret,
|
|
|
1155
1184
|
failedVideoNames,
|
|
1156
1185
|
failedVideoMessages,
|
|
1157
1186
|
studioNotices,
|
|
1187
|
+
plan: resolvedPlan,
|
|
1158
1188
|
};
|
|
1159
1189
|
}
|
|
1160
1190
|
finally {
|
|
@@ -1417,8 +1447,10 @@ async function requireScreenCISecret(configPath, opts = {}) {
|
|
|
1417
1447
|
if (!secret) {
|
|
1418
1448
|
// In a non-interactive session ensureScreenciSecret already printed the
|
|
1419
1449
|
// sign-in link and the next step, so we exit without repeating guidance.
|
|
1450
|
+
// A pending sign-in is an expected handoff (surface the link, then rerun),
|
|
1451
|
+
// not a failure, so exit cleanly (0) to avoid flagging the run as red.
|
|
1420
1452
|
if (opts.interactive === false) {
|
|
1421
|
-
process.exit(
|
|
1453
|
+
process.exit(0);
|
|
1422
1454
|
}
|
|
1423
1455
|
const envFilePath = await resolveProjectEnvFilePath(resolvedConfigPath);
|
|
1424
1456
|
logger.error(`No SCREENCI_SECRET configured. Rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} or add SCREENCI_SECRET manually to ${envFilePath} by following the guide at ${pc.cyan(SCREENCI_RECORD_DOCS_URL)}.`);
|
|
@@ -1556,7 +1588,7 @@ async function pollLinkSessionOnce(spec) {
|
|
|
1556
1588
|
}
|
|
1557
1589
|
return { status };
|
|
1558
1590
|
}
|
|
1559
|
-
async function pollLinkSession(spec) {
|
|
1591
|
+
async function pollLinkSession(spec, pollIntervalMs = SCREENCI_LINK_SESSION_POLL_INTERVAL_MS) {
|
|
1560
1592
|
for (;;) {
|
|
1561
1593
|
const result = await pollLinkSessionOnce(spec);
|
|
1562
1594
|
if (result.status === 'completed' && result.secret) {
|
|
@@ -1567,11 +1599,12 @@ async function pollLinkSession(spec) {
|
|
|
1567
1599
|
result.status === 'invalid') {
|
|
1568
1600
|
return result;
|
|
1569
1601
|
}
|
|
1570
|
-
await new Promise((resolveDelay) => setTimeout(resolveDelay,
|
|
1602
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, pollIntervalMs));
|
|
1571
1603
|
}
|
|
1572
1604
|
}
|
|
1573
1605
|
export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
1574
1606
|
const interactive = opts.interactive ?? true;
|
|
1607
|
+
const pollAuth = opts.pollAuth ?? false;
|
|
1575
1608
|
const existingSecret = process.env.SCREENCI_SECRET;
|
|
1576
1609
|
if (existingSecret)
|
|
1577
1610
|
return existingSecret;
|
|
@@ -1614,12 +1647,14 @@ export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
|
1614
1647
|
return secret;
|
|
1615
1648
|
};
|
|
1616
1649
|
if (!interactive) {
|
|
1617
|
-
// Non-interactive sessions cannot complete a browser sign-in here, so
|
|
1618
|
-
// never block. Reuse or create the persisted session and check
|
|
1619
|
-
// once; if a stored session is stale, recreate it and check
|
|
1620
|
-
// When the session is already completed (the sign-in happened
|
|
1621
|
-
// runs) we pick up the secret; otherwise we print the link and
|
|
1622
|
-
// the caller can surface it and rerun later.
|
|
1650
|
+
// Non-interactive sessions cannot complete a browser sign-in here, so by
|
|
1651
|
+
// default we never block. Reuse or create the persisted session and check
|
|
1652
|
+
// its status once; if a stored session is stale, recreate it and check
|
|
1653
|
+
// once more. When the session is already completed (the sign-in happened
|
|
1654
|
+
// between runs) we pick up the secret; otherwise we print the link and
|
|
1655
|
+
// return so the caller can surface it and rerun later. The exception is
|
|
1656
|
+
// `pollAuth` (the `--poll-auth` flag), which opts in to waiting: it keeps
|
|
1657
|
+
// polling on a slow cadence until sign-in completes.
|
|
1623
1658
|
let spec = await ensureSpec();
|
|
1624
1659
|
let result = await pollLinkSessionOnce(spec);
|
|
1625
1660
|
if (result.status === 'expired' ||
|
|
@@ -1632,8 +1667,25 @@ export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
|
1632
1667
|
if (result.status === 'completed' && result.secret) {
|
|
1633
1668
|
return await saveCompletedSecret(result.secret);
|
|
1634
1669
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1670
|
+
if (pollAuth) {
|
|
1671
|
+
// The caller asked us to wait for sign-in. Print the link, then poll on
|
|
1672
|
+
// a slow cadence until the human finishes signing in, and continue
|
|
1673
|
+
// recording automatically once the secret lands. We re-print the link
|
|
1674
|
+
// on every (re)created session so the latest valid link is always
|
|
1675
|
+
// visible, including after a stale session is recreated.
|
|
1676
|
+
for (;;) {
|
|
1677
|
+
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);
|
|
1680
|
+
if (polled.status === 'completed' && polled.secret) {
|
|
1681
|
+
return await saveCompletedSecret(polled.secret);
|
|
1682
|
+
}
|
|
1683
|
+
deletePersistedLinkSessionSpec(specPath);
|
|
1684
|
+
spec = await ensureSpec();
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
logger.info(`Sign-in required to record. Open this link to sign in:\n${pc.cyan(spec.appUrl)}\n` +
|
|
1688
|
+
`This session is non-interactive, so sign-in can't complete here. After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to continue, or run ${pc.cyan(getSuggestedScreenciCommand('record', '--poll-auth'))} to print the link again and wait for sign-in, continuing automatically.`);
|
|
1637
1689
|
return undefined;
|
|
1638
1690
|
}
|
|
1639
1691
|
for (;;) {
|
|
@@ -1669,12 +1721,13 @@ export async function main() {
|
|
|
1669
1721
|
.command('record [playwrightArgs...]')
|
|
1670
1722
|
.description('Record videos using Playwright')
|
|
1671
1723
|
.option('-v, --verbose', 'verbose output')
|
|
1724
|
+
.option('--poll-auth', 'wait for sign-in to complete (polling every 5s) instead of exiting, then continue recording')
|
|
1672
1725
|
.allowUnknownOption(true)
|
|
1673
1726
|
.action(async () => {
|
|
1674
1727
|
const parsed = parseRecordCliArgs(getSubcommandArgv('record'));
|
|
1675
1728
|
let playwrightFailure = null;
|
|
1676
1729
|
try {
|
|
1677
|
-
await run('record', parsed.otherArgs, parsed.configPath, parsed.verbose);
|
|
1730
|
+
await run('record', parsed.otherArgs, parsed.configPath, parsed.verbose, false, parsed.pollAuth);
|
|
1678
1731
|
}
|
|
1679
1732
|
catch (error) {
|
|
1680
1733
|
if (!(error instanceof Error))
|
|
@@ -1727,6 +1780,7 @@ export async function main() {
|
|
|
1727
1780
|
failedVideoNames: [],
|
|
1728
1781
|
failedVideoMessages: [],
|
|
1729
1782
|
studioNotices: [],
|
|
1783
|
+
plan: null,
|
|
1730
1784
|
};
|
|
1731
1785
|
try {
|
|
1732
1786
|
uploadResult = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, secret);
|
|
@@ -1737,7 +1791,7 @@ export async function main() {
|
|
|
1737
1791
|
}
|
|
1738
1792
|
throw err;
|
|
1739
1793
|
}
|
|
1740
|
-
const { projectId, recordId, hadFailures, failedVideoNames, failedVideoMessages, studioNotices, } = uploadResult;
|
|
1794
|
+
const { projectId, recordId, hadFailures, failedVideoNames, failedVideoMessages, studioNotices, plan, } = uploadResult;
|
|
1741
1795
|
if (recordId !== null && projectId !== null) {
|
|
1742
1796
|
const recordUrl = `${appUrl}/record/${recordId}`;
|
|
1743
1797
|
await writeGitHubProjectOutput(recordUrl);
|
|
@@ -1756,6 +1810,11 @@ export async function main() {
|
|
|
1756
1810
|
: 'Recording finished, rendering in progress. Results available at:');
|
|
1757
1811
|
logger.info(pc.cyan(projectUrl));
|
|
1758
1812
|
}
|
|
1813
|
+
if (projectId !== null && plan !== 'business') {
|
|
1814
|
+
logger.info('');
|
|
1815
|
+
logger.info('Upgrade for more renders, more active videos, and expressive narration:');
|
|
1816
|
+
logger.info(pc.cyan(`${appUrl}/select-plan`));
|
|
1817
|
+
}
|
|
1759
1818
|
for (const notice of studioNotices) {
|
|
1760
1819
|
if ('held' in notice.studio) {
|
|
1761
1820
|
logger.info('');
|
|
@@ -1900,6 +1959,7 @@ function getSubcommandArgv(command) {
|
|
|
1900
1959
|
function parseRecordCliArgs(args) {
|
|
1901
1960
|
let configPath;
|
|
1902
1961
|
let verbose = false;
|
|
1962
|
+
let pollAuth = false;
|
|
1903
1963
|
const otherArgs = [];
|
|
1904
1964
|
for (let i = 0; i < args.length; i++) {
|
|
1905
1965
|
const arg = args[i];
|
|
@@ -1917,6 +1977,9 @@ function parseRecordCliArgs(args) {
|
|
|
1917
1977
|
else if (arg === '--verbose' || arg === '-v') {
|
|
1918
1978
|
verbose = true;
|
|
1919
1979
|
}
|
|
1980
|
+
else if (arg === '--poll-auth') {
|
|
1981
|
+
pollAuth = true;
|
|
1982
|
+
}
|
|
1920
1983
|
else {
|
|
1921
1984
|
otherArgs.push(arg);
|
|
1922
1985
|
}
|
|
@@ -1924,6 +1987,7 @@ function parseRecordCliArgs(args) {
|
|
|
1924
1987
|
return {
|
|
1925
1988
|
configPath,
|
|
1926
1989
|
verbose,
|
|
1990
|
+
pollAuth,
|
|
1927
1991
|
otherArgs,
|
|
1928
1992
|
};
|
|
1929
1993
|
}
|
|
@@ -1974,7 +2038,7 @@ function validateArgs(args) {
|
|
|
1974
2038
|
}
|
|
1975
2039
|
}
|
|
1976
2040
|
}
|
|
1977
|
-
async function run(command, additionalArgs, customConfigPath, verbose = false, mockRecord = false) {
|
|
2041
|
+
async function run(command, additionalArgs, customConfigPath, verbose = false, mockRecord = false, pollAuth = false) {
|
|
1978
2042
|
const configPath = resolveScreenCIConfigPathOrExit(customConfigPath);
|
|
1979
2043
|
if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
|
|
1980
2044
|
await loadEnvFileFromConfigSource(configPath, false);
|
|
@@ -1984,6 +2048,7 @@ async function run(command, additionalArgs, customConfigPath, verbose = false, m
|
|
|
1984
2048
|
if (!process.env.SCREENCI_SECRET) {
|
|
1985
2049
|
await requireScreenCISecret(configPath, {
|
|
1986
2050
|
interactive: detectInteractiveSession(),
|
|
2051
|
+
pollAuth,
|
|
1987
2052
|
});
|
|
1988
2053
|
}
|
|
1989
2054
|
validateArgs(additionalArgs);
|