screenci 0.0.72 → 0.0.73
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 +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +126 -73
- package/dist/cli.js.map +1 -1
- package/dist/src/asset.d.ts +74 -28
- package/dist/src/asset.d.ts.map +1 -1
- package/dist/src/asset.js +83 -14
- package/dist/src/asset.js.map +1 -1
- package/dist/src/config.d.ts +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +13 -7
- package/dist/src/config.js.map +1 -1
- package/dist/src/defaults.d.ts +2 -2
- package/dist/src/defaults.d.ts.map +1 -1
- package/dist/src/defaults.js +2 -2
- package/dist/src/defaults.js.map +1 -1
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +66 -26
- package/dist/src/init.js.map +1 -1
- package/dist/src/mouse.d.ts.map +1 -1
- package/dist/src/mouse.js +6 -0
- package/dist/src/mouse.js.map +1 -1
- package/dist/src/runtimeContext.d.ts +19 -0
- package/dist/src/runtimeContext.d.ts.map +1 -1
- package/dist/src/runtimeContext.js +11 -0
- package/dist/src/runtimeContext.js.map +1 -1
- package/dist/src/screenshot.d.ts.map +1 -1
- package/dist/src/screenshot.js +2 -0
- package/dist/src/screenshot.js.map +1 -1
- package/dist/src/stillCapture.d.ts +8 -1
- package/dist/src/stillCapture.d.ts.map +1 -1
- package/dist/src/stillCapture.js +11 -0
- package/dist/src/stillCapture.js.map +1 -1
- package/dist/src/types.d.ts +2 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/video.d.ts.map +1 -1
- package/dist/src/video.js +7 -1
- package/dist/src/video.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/skills/screenci/SKILL.md +12 -17
- package/skills/screenci/references/init.md +1 -1
- package/skills/screenci/references/record.md +6 -6
- package/dist/docs/video-sources/product-pitch.screenci.d.ts +0 -2
- package/dist/docs/video-sources/product-pitch.screenci.d.ts.map +0 -1
- package/dist/docs/video-sources/product-pitch.screenci.js +0 -201
- package/dist/docs/video-sources/product-pitch.screenci.js.map +0 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ already know Playwright locators, navigation, and waiting, you already know
|
|
|
41
41
|
most of the automation layer.
|
|
42
42
|
|
|
43
43
|
```ts
|
|
44
|
-
//
|
|
44
|
+
// recordings/onboarding.screenci.ts
|
|
45
45
|
import { hide, speed, time, video } from 'screenci'
|
|
46
46
|
|
|
47
47
|
video('Onboarding flow', async ({ page }) => {
|
package/dist/cli.d.ts
CHANGED
|
@@ -45,6 +45,9 @@ export type StudioUploadNotice = {
|
|
|
45
45
|
};
|
|
46
46
|
export declare function formatStudioUrl(appUrl: string, projectId: string, videoId: string): string;
|
|
47
47
|
type OrgPlan = 'free' | 'starter' | 'business';
|
|
48
|
+
export declare class SignInTimeoutError extends Error {
|
|
49
|
+
constructor(message: string);
|
|
50
|
+
}
|
|
48
51
|
export declare function withUploadRetry<T>(fn: () => Promise<T>, signal: AbortSignal | undefined, maxAttempts?: number): Promise<T>;
|
|
49
52
|
export declare function attachUploadAbortStdinListener(input: Pick<NodeJS.ReadStream, 'on' | 'off' | 'pause'>, onAbort: (signal: NodeJS.Signals) => void): () => void;
|
|
50
53
|
export declare function collectUploadAssets(data: RecordingData, configDir: string): Promise<PreparedUploadAsset[]>;
|
|
@@ -53,6 +56,7 @@ export declare function annotateRecordingDataWithAssetHashes(data: RecordingData
|
|
|
53
56
|
export declare function formatUploadStartFailureMessage(videoName: string, status: number, responseText: string, secret: string): string;
|
|
54
57
|
export declare function formatFailedVideoMessage(videoName: string, message: string): string;
|
|
55
58
|
export declare function printUploadStartFailureMessage(videoName: string, status: number, responseText: string, secret: string): void;
|
|
59
|
+
export declare function displayAssetPath(assetPath: string): string;
|
|
56
60
|
export declare function uploadRecordings(screenciDir: string, projectName: string, apiUrl: string, secret: string, specificEntry?: string, verbose?: boolean): Promise<{
|
|
57
61
|
projectId: string | null;
|
|
58
62
|
recordId: string | null;
|
|
@@ -73,12 +77,14 @@ export declare function extractGrep(args: string[]): string | undefined;
|
|
|
73
77
|
export declare function ensureScreenciSecret(resolvedConfigPath?: string, opts?: {
|
|
74
78
|
interactive?: boolean;
|
|
75
79
|
pollAuth?: boolean;
|
|
80
|
+
noPollAuth?: boolean;
|
|
76
81
|
}): Promise<string | undefined>;
|
|
77
82
|
export declare function main(): Promise<void>;
|
|
78
83
|
export declare function parseRecordCliArgs(args: string[]): {
|
|
79
84
|
configPath: string | undefined;
|
|
80
85
|
verbose: boolean;
|
|
81
86
|
pollAuth: boolean;
|
|
87
|
+
noPollAuth: boolean;
|
|
82
88
|
remote: boolean;
|
|
83
89
|
languages: string | undefined;
|
|
84
90
|
otherArgs: string[];
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync,
|
|
|
4
4
|
import { createHash, randomUUID } from 'crypto';
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
6
|
import { appendFile, readdir, readFile, stat, writeFile } from 'fs/promises';
|
|
7
|
-
import { delimiter, dirname, relative as pathRelative, resolve } from 'path';
|
|
7
|
+
import { delimiter, dirname, isAbsolute, relative as pathRelative, resolve, } from 'path';
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
9
9
|
import { Command, CommanderError } from 'commander';
|
|
10
10
|
import pc from 'picocolors';
|
|
@@ -27,11 +27,15 @@ const SCREENCI_LINK_SESSION_POLL_INTERVAL_MS = 2_000;
|
|
|
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
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
30
|
+
// A waiting `record` does not wait forever: after this long without a completed
|
|
31
|
+
// sign-in we stop polling so a non-interactive agent run does not hang. The link
|
|
32
|
+
// stays valid, so the command can be rerun. The default can be overridden with
|
|
33
|
+
// SCREENCI_POLL_AUTH_TIMEOUT_MS (milliseconds).
|
|
34
34
|
const SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
35
|
+
// An interactive `record` (a human at a terminal) waits longer before giving up,
|
|
36
|
+
// since a person may take a while to open the link and finish signing in. Also
|
|
37
|
+
// overridable with SCREENCI_POLL_AUTH_TIMEOUT_MS.
|
|
38
|
+
const SCREENCI_LINK_SESSION_INTERACTIVE_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
35
39
|
const require = createRequire(import.meta.url);
|
|
36
40
|
/**
|
|
37
41
|
* Reports whether the current session can complete an interactive browser
|
|
@@ -184,7 +188,7 @@ function resolveRecordingFileCandidates(filePath, configDir, sourceFilePath) {
|
|
|
184
188
|
return [
|
|
185
189
|
filePath,
|
|
186
190
|
...(sourceFileCandidate ? [sourceFileCandidate] : []),
|
|
187
|
-
resolve(configDir, '
|
|
191
|
+
resolve(configDir, 'recordings', filePath),
|
|
188
192
|
resolve(configDir, pathRelative('/app', filePath)),
|
|
189
193
|
];
|
|
190
194
|
}
|
|
@@ -243,6 +247,17 @@ class RecordFailureHintError extends Error {
|
|
|
243
247
|
this.cause = cause;
|
|
244
248
|
}
|
|
245
249
|
}
|
|
250
|
+
// Thrown when we wait for a browser sign-in and the deadline passes before it
|
|
251
|
+
// completes. The link stays valid, so the message tells the user to sign in and
|
|
252
|
+
// rerun. A timed-out sign-in means recording did not happen, so the caller maps
|
|
253
|
+
// this to a non-zero exit (it is a failure, not the clean print-and-exit handoff
|
|
254
|
+
// used by CI).
|
|
255
|
+
export class SignInTimeoutError extends Error {
|
|
256
|
+
constructor(message) {
|
|
257
|
+
super(message);
|
|
258
|
+
this.name = 'SignInTimeoutError';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
246
261
|
function isUploadCancelledError(err) {
|
|
247
262
|
return (err instanceof UploadCancelledError ||
|
|
248
263
|
(err instanceof Error &&
|
|
@@ -1017,6 +1032,13 @@ export function printUploadStartFailureMessage(videoName, status, responseText,
|
|
|
1017
1032
|
}
|
|
1018
1033
|
logger.warn(message);
|
|
1019
1034
|
}
|
|
1035
|
+
export function displayAssetPath(assetPath) {
|
|
1036
|
+
if (!isAbsolute(assetPath)) {
|
|
1037
|
+
return assetPath;
|
|
1038
|
+
}
|
|
1039
|
+
const rel = pathRelative(process.cwd(), assetPath);
|
|
1040
|
+
return rel.length > 0 ? rel : assetPath;
|
|
1041
|
+
}
|
|
1020
1042
|
async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIfAborted, progressReporter) {
|
|
1021
1043
|
const logInfo = (message) => {
|
|
1022
1044
|
if (progressReporter) {
|
|
@@ -1050,7 +1072,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1050
1072
|
}
|
|
1051
1073
|
const checkBody = (await checkRes.json());
|
|
1052
1074
|
if (checkBody.exists) {
|
|
1053
|
-
logInfo(`${pc.green('✔')} Asset already exists: ${asset.path}`);
|
|
1075
|
+
logInfo(`${pc.green('✔')} Asset already exists: ${displayAssetPath(asset.path)}`);
|
|
1054
1076
|
continue;
|
|
1055
1077
|
}
|
|
1056
1078
|
if (!asset.fileBuffer || !asset.contentType) {
|
|
@@ -1077,14 +1099,14 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1077
1099
|
if (!res.ok) {
|
|
1078
1100
|
const text = await res.text();
|
|
1079
1101
|
if (res.status === 409 && text.includes('already exists')) {
|
|
1080
|
-
logInfo(`${pc.green('✔')} Asset already exists: ${asset.path}`);
|
|
1102
|
+
logInfo(`${pc.green('✔')} Asset already exists: ${displayAssetPath(asset.path)}`);
|
|
1081
1103
|
}
|
|
1082
1104
|
else {
|
|
1083
1105
|
throw new UploadAssetError(`Failed to upload asset ${asset.path}: ${res.status} ${text}${hint401(res.status, secret)}`);
|
|
1084
1106
|
}
|
|
1085
1107
|
}
|
|
1086
1108
|
else {
|
|
1087
|
-
logInfo(`Asset uploaded: ${asset.path}`);
|
|
1109
|
+
logInfo(`Asset uploaded: ${displayAssetPath(asset.path)}`);
|
|
1088
1110
|
}
|
|
1089
1111
|
}
|
|
1090
1112
|
catch (err) {
|
|
@@ -1387,8 +1409,22 @@ async function loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath) {
|
|
|
1387
1409
|
}
|
|
1388
1410
|
async function requireScreenCISecret(configPath, opts = {}) {
|
|
1389
1411
|
const { resolvedConfigPath, screenciConfig } = await loadScreenCIConfigAndEnv(configPath);
|
|
1390
|
-
|
|
1391
|
-
|
|
1412
|
+
let secret;
|
|
1413
|
+
try {
|
|
1414
|
+
secret =
|
|
1415
|
+
process.env.SCREENCI_SECRET ??
|
|
1416
|
+
(await ensureScreenciSecret(resolvedConfigPath, opts));
|
|
1417
|
+
}
|
|
1418
|
+
catch (err) {
|
|
1419
|
+
// A waited-out sign-in means recording did not happen: print the (already
|
|
1420
|
+
// formatted) message and exit non-zero so the failure is loud, instead of
|
|
1421
|
+
// looking like a successful run.
|
|
1422
|
+
if (err instanceof SignInTimeoutError) {
|
|
1423
|
+
logger.error(err.message);
|
|
1424
|
+
process.exit(1);
|
|
1425
|
+
}
|
|
1426
|
+
throw err;
|
|
1427
|
+
}
|
|
1392
1428
|
if (!secret) {
|
|
1393
1429
|
// In a non-interactive session ensureScreenciSecret already printed the
|
|
1394
1430
|
// sign-in link and the next step, so we exit without repeating guidance.
|
|
@@ -1567,14 +1603,12 @@ async function pollLinkSessionOnce(spec) {
|
|
|
1567
1603
|
}
|
|
1568
1604
|
return { status };
|
|
1569
1605
|
}
|
|
1570
|
-
function
|
|
1606
|
+
function getAuthPollTimeoutMs(defaultMs) {
|
|
1571
1607
|
const raw = process.env.SCREENCI_POLL_AUTH_TIMEOUT_MS;
|
|
1572
1608
|
if (raw === undefined)
|
|
1573
|
-
return
|
|
1609
|
+
return defaultMs;
|
|
1574
1610
|
const parsed = Number(raw);
|
|
1575
|
-
return Number.isFinite(parsed) && parsed >= 0
|
|
1576
|
-
? parsed
|
|
1577
|
-
: SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS;
|
|
1611
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : defaultMs;
|
|
1578
1612
|
}
|
|
1579
1613
|
async function pollLinkSession(spec, pollIntervalMs = SCREENCI_LINK_SESSION_POLL_INTERVAL_MS, deadlineEpochMs) {
|
|
1580
1614
|
for (;;) {
|
|
@@ -1598,6 +1632,7 @@ async function pollLinkSession(spec, pollIntervalMs = SCREENCI_LINK_SESSION_POLL
|
|
|
1598
1632
|
export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
1599
1633
|
const interactive = opts.interactive ?? true;
|
|
1600
1634
|
const pollAuth = opts.pollAuth ?? false;
|
|
1635
|
+
const noPollAuth = opts.noPollAuth ?? false;
|
|
1601
1636
|
const existingSecret = process.env.SCREENCI_SECRET;
|
|
1602
1637
|
if (existingSecret)
|
|
1603
1638
|
return existingSecret;
|
|
@@ -1639,69 +1674,80 @@ export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
|
1639
1674
|
logger.info(`Successfully saved SCREENCI_SECRET to ${envFilePath}`);
|
|
1640
1675
|
return secret;
|
|
1641
1676
|
};
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
}
|
|
1681
|
-
if (polled.status === 'timed-out' || Date.now() >= deadlineEpochMs) {
|
|
1682
|
-
logger.info(`Timed out after ${timeoutMinutes} minutes waiting for sign-in. The link is still valid:\n${pc.cyan(spec.appUrl)}\n` +
|
|
1683
|
-
`After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to continue, or ${pc.cyan(getSuggestedScreenciCommand('record', '--poll-auth'))} to wait again.`);
|
|
1684
|
-
return undefined;
|
|
1685
|
-
}
|
|
1686
|
-
deletePersistedLinkSessionSpec(specPath);
|
|
1687
|
-
spec = await ensureSpec();
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1677
|
+
// Decide whether this run waits for a browser sign-in or just prints the
|
|
1678
|
+
// link and hands off. A human at a terminal always waits. A plain
|
|
1679
|
+
// non-interactive run (a coding agent: no terminal, no CI) also waits by
|
|
1680
|
+
// default, so `npx screenci record` records without needing a flag. CI and
|
|
1681
|
+
// an explicit SCREENCI_NONINTERACTIVE never wait (CI must not hang and is
|
|
1682
|
+
// expected to provide SCREENCI_SECRET), and `--no-poll-auth` opts back into
|
|
1683
|
+
// that print-and-exit handoff. `--poll-auth` forces waiting and is kept for
|
|
1684
|
+
// backwards compatibility (it is the default for agents now).
|
|
1685
|
+
const isCI = process.env.CI === 'true';
|
|
1686
|
+
const nonInteractiveForced = process.env.SCREENCI_NONINTERACTIVE === '1';
|
|
1687
|
+
const waitForSignIn = noPollAuth
|
|
1688
|
+
? false
|
|
1689
|
+
: interactive || pollAuth || (!isCI && !nonInteractiveForced);
|
|
1690
|
+
const pollIntervalMs = interactive
|
|
1691
|
+
? SCREENCI_LINK_SESSION_POLL_INTERVAL_MS
|
|
1692
|
+
: SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS;
|
|
1693
|
+
const timeoutMs = getAuthPollTimeoutMs(interactive
|
|
1694
|
+
? SCREENCI_LINK_SESSION_INTERACTIVE_TIMEOUT_MS
|
|
1695
|
+
: SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS);
|
|
1696
|
+
// Check the current/stored session once. If sign-in already completed (for
|
|
1697
|
+
// example between runs) we pick up the secret immediately. Recreate a stale
|
|
1698
|
+
// session and check once more before deciding to wait or hand off.
|
|
1699
|
+
let spec = await ensureSpec();
|
|
1700
|
+
let result = await pollLinkSessionOnce(spec);
|
|
1701
|
+
if (result.status === 'expired' ||
|
|
1702
|
+
result.status === 'consumed' ||
|
|
1703
|
+
result.status === 'invalid') {
|
|
1704
|
+
deletePersistedLinkSessionSpec(specPath);
|
|
1705
|
+
spec = await ensureSpec();
|
|
1706
|
+
result = await pollLinkSessionOnce(spec);
|
|
1707
|
+
}
|
|
1708
|
+
if (result.status === 'completed' && result.secret) {
|
|
1709
|
+
return await saveCompletedSecret(result.secret);
|
|
1710
|
+
}
|
|
1711
|
+
if (!waitForSignIn) {
|
|
1712
|
+
// CI / SCREENCI_NONINTERACTIVE / --no-poll-auth: print the link and hand
|
|
1713
|
+
// off without waiting. The caller exits cleanly (0); a pending sign-in
|
|
1714
|
+
// here is an expected handoff, not a failure.
|
|
1690
1715
|
logger.info(`Sign-in required to record. Open this link to sign in:\n${pc.cyan(spec.appUrl)}\n` +
|
|
1691
|
-
`This session
|
|
1716
|
+
`This session won't wait for sign-in. After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to record. In CI, set SCREENCI_SECRET (from ${getScreenCISecretsUrl()}) to record without an interactive sign-in.`);
|
|
1692
1717
|
return undefined;
|
|
1693
1718
|
}
|
|
1719
|
+
// Wait for sign-in: print the link, then poll until the human finishes and
|
|
1720
|
+
// continue recording automatically once the secret lands. We re-print the
|
|
1721
|
+
// link on every (re)created session so the latest valid link is always
|
|
1722
|
+
// visible. We do not wait forever: after the timeout we throw so the caller
|
|
1723
|
+
// exits non-zero (a timed-out sign-in means nothing was recorded). The link
|
|
1724
|
+
// stays valid for a rerun.
|
|
1725
|
+
const timeoutMinutes = Math.max(1, Math.round(timeoutMs / 60_000));
|
|
1726
|
+
const intervalSeconds = Math.max(1, Math.round(pollIntervalMs / 1_000));
|
|
1727
|
+
const deadlineEpochMs = Date.now() + timeoutMs;
|
|
1694
1728
|
for (;;) {
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
const
|
|
1698
|
-
if (
|
|
1699
|
-
return await saveCompletedSecret(
|
|
1729
|
+
logger.info(`Sign-in required to record. Open this link to sign in:\n${pc.cyan(spec.appUrl)}\n` +
|
|
1730
|
+
`Waiting for sign-in (checking every ${intervalSeconds} seconds, up to ${timeoutMinutes} minutes). Recording continues automatically once you finish.`);
|
|
1731
|
+
const polled = await pollLinkSession(spec, pollIntervalMs, deadlineEpochMs);
|
|
1732
|
+
if (polled.status === 'completed' && polled.secret) {
|
|
1733
|
+
return await saveCompletedSecret(polled.secret);
|
|
1700
1734
|
}
|
|
1735
|
+
if (polled.status === 'timed-out' || Date.now() >= deadlineEpochMs) {
|
|
1736
|
+
throw new SignInTimeoutError(`Timed out after ${timeoutMinutes} minutes waiting for sign-in. The link is still valid:\n${pc.cyan(spec.appUrl)}\n` +
|
|
1737
|
+
`After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to record.`);
|
|
1738
|
+
}
|
|
1739
|
+
// Session went stale (expired/consumed/invalid) before sign-in: recreate
|
|
1740
|
+
// and keep waiting until the deadline.
|
|
1701
1741
|
deletePersistedLinkSessionSpec(specPath);
|
|
1742
|
+
spec = await ensureSpec();
|
|
1702
1743
|
}
|
|
1703
1744
|
}
|
|
1704
1745
|
catch (err) {
|
|
1746
|
+
// A timed-out sign-in is a deliberate, already-formatted failure: let it
|
|
1747
|
+
// propagate so the caller can exit non-zero rather than swallowing it as a
|
|
1748
|
+
// generic auth error.
|
|
1749
|
+
if (err instanceof SignInTimeoutError)
|
|
1750
|
+
throw err;
|
|
1705
1751
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1706
1752
|
logger.warn(`Authentication failed: ${msg}`);
|
|
1707
1753
|
logger.info(`You can add SCREENCI_SECRET manually to ${resolvedConfigPath ? await resolveProjectEnvFilePath(resolvedConfigPath) : '.env'} later. Get it from ${getScreenCISecretsUrl()}.`);
|
|
@@ -1724,7 +1770,8 @@ export async function main() {
|
|
|
1724
1770
|
.command('record [playwrightArgs...]')
|
|
1725
1771
|
.description('Record videos using Playwright')
|
|
1726
1772
|
.option('-v, --verbose', 'verbose output')
|
|
1727
|
-
.option('--poll-auth', 'wait for sign-in
|
|
1773
|
+
.option('--poll-auth', 'wait for sign-in before recording (now the default off-CI; kept for backwards compatibility)')
|
|
1774
|
+
.option('--no-poll-auth', 'do not wait for sign-in: print the link and exit cleanly if not signed in (the default under CI)')
|
|
1728
1775
|
.option('--remote', 'trigger the GitHub Actions recording workflow for this project remotely instead of recording locally')
|
|
1729
1776
|
.option('--languages <langs>', 'record/render only these languages (comma-separated, e.g. fi,en)')
|
|
1730
1777
|
.allowUnknownOption(true)
|
|
@@ -1740,7 +1787,7 @@ export async function main() {
|
|
|
1740
1787
|
}
|
|
1741
1788
|
let playwrightFailure = null;
|
|
1742
1789
|
try {
|
|
1743
|
-
await run('record', parsed.otherArgs, parsed.configPath, parsed.verbose, false, parsed.pollAuth, parsed.languages);
|
|
1790
|
+
await run('record', parsed.otherArgs, parsed.configPath, parsed.verbose, false, parsed.pollAuth, parsed.languages, parsed.noPollAuth);
|
|
1744
1791
|
}
|
|
1745
1792
|
catch (error) {
|
|
1746
1793
|
if (!(error instanceof Error))
|
|
@@ -1977,6 +2024,7 @@ export function parseRecordCliArgs(args) {
|
|
|
1977
2024
|
let configPath;
|
|
1978
2025
|
let verbose = false;
|
|
1979
2026
|
let pollAuth = false;
|
|
2027
|
+
let noPollAuth = false;
|
|
1980
2028
|
let remote = false;
|
|
1981
2029
|
let languages;
|
|
1982
2030
|
const otherArgs = [];
|
|
@@ -2013,6 +2061,9 @@ export function parseRecordCliArgs(args) {
|
|
|
2013
2061
|
else if (arg === '--poll-auth') {
|
|
2014
2062
|
pollAuth = true;
|
|
2015
2063
|
}
|
|
2064
|
+
else if (arg === '--no-poll-auth') {
|
|
2065
|
+
noPollAuth = true;
|
|
2066
|
+
}
|
|
2016
2067
|
else if (arg === '--remote') {
|
|
2017
2068
|
remote = true;
|
|
2018
2069
|
}
|
|
@@ -2024,6 +2075,7 @@ export function parseRecordCliArgs(args) {
|
|
|
2024
2075
|
configPath,
|
|
2025
2076
|
verbose,
|
|
2026
2077
|
pollAuth,
|
|
2078
|
+
noPollAuth,
|
|
2027
2079
|
remote,
|
|
2028
2080
|
languages,
|
|
2029
2081
|
otherArgs,
|
|
@@ -2154,7 +2206,7 @@ async function fetchRecordOptionsEnv(configPath, verbose) {
|
|
|
2154
2206
|
return {};
|
|
2155
2207
|
}
|
|
2156
2208
|
}
|
|
2157
|
-
async function run(command, additionalArgs, customConfigPath, verbose = false, mockRecord = false, pollAuth = false, languages) {
|
|
2209
|
+
async function run(command, additionalArgs, customConfigPath, verbose = false, mockRecord = false, pollAuth = false, languages, noPollAuth = false) {
|
|
2158
2210
|
const configPath = resolveScreenCIConfigPathOrExit(customConfigPath);
|
|
2159
2211
|
if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
|
|
2160
2212
|
await loadEnvFileFromConfigSource(configPath, false);
|
|
@@ -2165,6 +2217,7 @@ async function run(command, additionalArgs, customConfigPath, verbose = false, m
|
|
|
2165
2217
|
await requireScreenCISecret(configPath, {
|
|
2166
2218
|
interactive: detectInteractiveSession(),
|
|
2167
2219
|
pollAuth,
|
|
2220
|
+
noPollAuth,
|
|
2168
2221
|
});
|
|
2169
2222
|
}
|
|
2170
2223
|
validateArgs(additionalArgs);
|