screenci 0.0.79 → 0.0.80
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 +12 -12
- package/dist/cli.d.ts +7 -21
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +23 -359
- package/dist/cli.js.map +1 -1
- package/dist/src/init.d.ts +10 -10
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +70 -43
- package/dist/src/init.js.map +1 -1
- package/dist/src/linkSession.d.ts +25 -28
- package/dist/src/linkSession.d.ts.map +1 -1
- package/dist/src/linkSession.js +74 -58
- package/dist/src/linkSession.js.map +1 -1
- package/dist/src/localize.d.ts +3 -2
- package/dist/src/localize.d.ts.map +1 -1
- package/dist/src/localize.js +3 -3
- package/dist/src/localize.js.map +1 -1
- package/dist/src/video.d.ts +7 -9
- package/dist/src/video.d.ts.map +1 -1
- package/dist/src/video.js +5 -7
- package/dist/src/video.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/skills/screenci/SKILL.md +32 -41
- package/skills/screenci/references/init.md +7 -7
- package/skills/screenci/references/record.md +8 -7
- package/dist/src/openBrowser.d.ts +0 -34
- package/dist/src/openBrowser.d.ts.map +0 -1
- package/dist/src/openBrowser.js +0 -62
- package/dist/src/openBrowser.js.map +0 -1
package/README.md
CHANGED
|
@@ -71,18 +71,18 @@ but remap its rendered duration.
|
|
|
71
71
|
|
|
72
72
|
## Authoring helpers
|
|
73
73
|
|
|
74
|
-
| Export
|
|
75
|
-
|
|
|
76
|
-
| `defineConfig`
|
|
77
|
-
| `video`
|
|
78
|
-
| `video.
|
|
79
|
-
| `hide`
|
|
80
|
-
| `autoZoom`
|
|
81
|
-
| `zoomTo`
|
|
82
|
-
| `resetZoom`
|
|
83
|
-
| `createOverlays`
|
|
84
|
-
| `voices`
|
|
85
|
-
| `modelTypes`
|
|
74
|
+
| Export | What it does |
|
|
75
|
+
| ----------------- | ------------------------------------------------------------ |
|
|
76
|
+
| `defineConfig` | Wraps Playwright config with ScreenCI defaults |
|
|
77
|
+
| `video` | Declares a video recording test |
|
|
78
|
+
| `video.narration` | Declares narration cues (per language or shared) for a video |
|
|
79
|
+
| `hide` | Cuts setup or cleanup out of the visible recording |
|
|
80
|
+
| `autoZoom` | Smooth camera follow for an interaction block |
|
|
81
|
+
| `zoomTo` | Manual camera framing for a locator or point |
|
|
82
|
+
| `resetZoom` | Returns from manual framing to the full viewport |
|
|
83
|
+
| `createOverlays` | Inserts timed media overlays into the recording timeline |
|
|
84
|
+
| `voices` | Available voice constants such as `voices.Ava` |
|
|
85
|
+
| `modelTypes` | Narration model constants |
|
|
86
86
|
|
|
87
87
|
See the [docs](https://screenci.com/docs) for configuration, narration,
|
|
88
88
|
camera, and CI setup.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RecordingCustomVoiceRef, RecordingData } from './src/events.js';
|
|
2
2
|
import type { VoiceKey } from './src/voices.js';
|
|
3
|
-
import type { RecordUploadPolicy } from './src/types.js';
|
|
3
|
+
import type { RecordUploadPolicy, ScreenCIConfig } from './src/types.js';
|
|
4
4
|
import { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl } from './src/linkSession.js';
|
|
5
5
|
export { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl };
|
|
6
6
|
type PlaywrightListReportSuite = {
|
|
@@ -51,9 +51,6 @@ export type StudioUploadNotice = {
|
|
|
51
51
|
};
|
|
52
52
|
export declare function formatStudioUrl(appUrl: string, projectId: string, videoId: string): string;
|
|
53
53
|
type OrgPlan = 'free' | 'starter' | 'business';
|
|
54
|
-
export declare class SignInTimeoutError extends Error {
|
|
55
|
-
constructor(message: string);
|
|
56
|
-
}
|
|
57
54
|
export declare function withUploadRetry<T>(fn: () => Promise<T>, signal: AbortSignal | undefined, maxAttempts?: number): Promise<T>;
|
|
58
55
|
export declare function attachUploadAbortStdinListener(input: Pick<NodeJS.ReadStream, 'on' | 'off' | 'pause'>, onAbort: (signal: NodeJS.Signals) => void): () => void;
|
|
59
56
|
export declare function clearRecordingDirectories(dir: string): void;
|
|
@@ -131,28 +128,17 @@ export declare function extractConfigStringLiteral(configSource: string, propert
|
|
|
131
128
|
export declare function extractRecordUploadPolicyLiteral(configSource: string): RecordUploadPolicy | undefined;
|
|
132
129
|
export declare function extractMockRecordLiteral(configSource: string): boolean | undefined;
|
|
133
130
|
export declare function getConfigModuleSpecifier(resolvedConfigPath: string): string;
|
|
131
|
+
export declare function requireScreenCISecret(configPath?: string): Promise<{
|
|
132
|
+
resolvedConfigPath: string;
|
|
133
|
+
screenciConfig: ScreenCIConfig;
|
|
134
|
+
secret: string;
|
|
135
|
+
apiUrl: string;
|
|
136
|
+
}>;
|
|
134
137
|
export declare function extractGrep(args: string[]): string | undefined;
|
|
135
|
-
export declare function ensureScreenciSecret(resolvedConfigPath?: string, opts?: {
|
|
136
|
-
interactive?: boolean;
|
|
137
|
-
pollAuth?: boolean;
|
|
138
|
-
noPollAuth?: boolean;
|
|
139
|
-
}): Promise<string | undefined>;
|
|
140
|
-
/**
|
|
141
|
-
* `screenci login`: get a sign-in link in front of a human as early as
|
|
142
|
-
* possible. It reuses the link `init` (or a prior run) persisted, or creates a
|
|
143
|
-
* fresh one, tries to open it in the browser best-effort, and always prints it
|
|
144
|
-
* as a fallback. It does NOT wait for sign-in to finish: the agent can run this,
|
|
145
|
-
* keep authoring and `test`ing, then `record` (which blocks for sign-in) once
|
|
146
|
-
* the script is green. If sign-in already completed, it saves the secret and
|
|
147
|
-
* exits.
|
|
148
|
-
*/
|
|
149
|
-
export declare function runLogin(configPath?: string): Promise<void>;
|
|
150
138
|
export declare function main(): Promise<void>;
|
|
151
139
|
export declare function parseRecordCliArgs(args: string[]): {
|
|
152
140
|
configPath: string | undefined;
|
|
153
141
|
verbose: boolean;
|
|
154
|
-
pollAuth: boolean;
|
|
155
|
-
noPollAuth: boolean;
|
|
156
142
|
remote: boolean;
|
|
157
143
|
languages: string | undefined;
|
|
158
144
|
otherArgs: string[];
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EACV,uBAAuB,EACvB,aAAa,EAEd,MAAM,iBAAiB,CAAA;AAUxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -13,9 +13,8 @@ import { determinePackageManager, parsePackageManager, runInit, } from './src/in
|
|
|
13
13
|
import { SCREENCI_DISABLE_RECORDING_TIMINGS_ENV, SCREENCI_MOCK_RECORD_ENV, SCREENCI_LANGUAGES_ENV, SCREENCI_VALUES_OVERRIDES_ENV, SCREENCI_RECORD_OPTIONS_ENV, isUploadExistingEnabled, } 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 {
|
|
16
|
+
import { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl, getScreenCISecretsUrl, } from './src/linkSession.js';
|
|
17
17
|
import { OVERLAY_CACHE_DIR_NAME } from './src/htmlRasterizer.js';
|
|
18
|
-
import { openUrlInBrowser } from './src/openBrowser.js';
|
|
19
18
|
import { maybeExtractVoiceSampleAudio } from './src/voiceSampleAudio.js';
|
|
20
19
|
// Re-export the environment-aware URL helpers so existing importers (and tests)
|
|
21
20
|
// can keep importing them from the CLI entrypoint.
|
|
@@ -25,20 +24,6 @@ const SCREENCI_RECORD_DOCS_URL = 'https://screenci.com/docs/reference/cli/#scree
|
|
|
25
24
|
// Records the recordId of the most recent `screenci record` upload so
|
|
26
25
|
// `screenci info` can report exactly the run that was just made.
|
|
27
26
|
const SCREENCI_LAST_RECORD_FILE = 'last-record.json';
|
|
28
|
-
const SCREENCI_LINK_SESSION_POLL_INTERVAL_MS = 2_000;
|
|
29
|
-
// `record --poll` keeps a non-interactive session (an agent or CI) waiting for
|
|
30
|
-
// sign-in. We poll on a slower cadence than the interactive loop so a long wait
|
|
31
|
-
// for a human to click the link does not hammer the backend.
|
|
32
|
-
const SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS = 5_000;
|
|
33
|
-
// A waiting `record` does not wait forever: after this long without a completed
|
|
34
|
-
// sign-in we stop polling so a non-interactive agent run does not hang. The link
|
|
35
|
-
// stays valid, so the command can be rerun. The default can be overridden with
|
|
36
|
-
// SCREENCI_POLL_AUTH_TIMEOUT_MS (milliseconds).
|
|
37
|
-
const SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
38
|
-
// An interactive `record` (a human at a terminal) waits longer before giving up,
|
|
39
|
-
// since a person may take a while to open the link and finish signing in. Also
|
|
40
|
-
// overridable with SCREENCI_POLL_AUTH_TIMEOUT_MS.
|
|
41
|
-
const SCREENCI_LINK_SESSION_INTERACTIVE_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
42
27
|
const require = createRequire(import.meta.url);
|
|
43
28
|
/**
|
|
44
29
|
* Reports whether the current session can complete an interactive browser
|
|
@@ -264,17 +249,6 @@ class RecordFailureHintError extends Error {
|
|
|
264
249
|
this.cause = cause;
|
|
265
250
|
}
|
|
266
251
|
}
|
|
267
|
-
// Thrown when we wait for a browser sign-in and the deadline passes before it
|
|
268
|
-
// completes. The link stays valid, so the message tells the user to sign in and
|
|
269
|
-
// rerun. A timed-out sign-in means recording did not happen, so the caller maps
|
|
270
|
-
// this to a non-zero exit (it is a failure, not the clean print-and-exit handoff
|
|
271
|
-
// used by CI).
|
|
272
|
-
export class SignInTimeoutError extends Error {
|
|
273
|
-
constructor(message) {
|
|
274
|
-
super(message);
|
|
275
|
-
this.name = 'SignInTimeoutError';
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
252
|
function isUploadCancelledError(err) {
|
|
279
253
|
return (err instanceof UploadCancelledError ||
|
|
280
254
|
(err instanceof Error &&
|
|
@@ -771,8 +745,6 @@ function forwardChildSignals(child, activityLabel, options = {}) {
|
|
|
771
745
|
export function clearRecordingDirectories(dir) {
|
|
772
746
|
mkdirSync(dir, { recursive: true });
|
|
773
747
|
for (const entry of readdirSync(dir)) {
|
|
774
|
-
if (entry === SCREENCI_LINK_SESSION_FILE)
|
|
775
|
-
continue;
|
|
776
748
|
// Preserve the cross-run overlay cache: it lives as a sibling of the
|
|
777
749
|
// per-recording directories so unchanged overlays are served byte for byte
|
|
778
750
|
// from a previous run. Wiping it would re-render and re-encode every overlay
|
|
@@ -1575,9 +1547,6 @@ async function countCompletedRecordings(screenciDir) {
|
|
|
1575
1547
|
}
|
|
1576
1548
|
return entries.filter((entry) => existsSync(resolve(screenciDir, entry, 'data.json'))).length;
|
|
1577
1549
|
}
|
|
1578
|
-
function getScreenCISecretsUrl() {
|
|
1579
|
-
return `${getDevFrontendUrl()}/secrets`;
|
|
1580
|
-
}
|
|
1581
1550
|
async function writeGitHubProjectOutput(projectUrl) {
|
|
1582
1551
|
const githubOutput = process.env.GITHUB_OUTPUT;
|
|
1583
1552
|
if (!githubOutput)
|
|
@@ -1787,34 +1756,15 @@ async function loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath) {
|
|
|
1787
1756
|
throw err;
|
|
1788
1757
|
}
|
|
1789
1758
|
}
|
|
1790
|
-
async function requireScreenCISecret(configPath
|
|
1759
|
+
export async function requireScreenCISecret(configPath) {
|
|
1791
1760
|
const { resolvedConfigPath, screenciConfig } = await loadScreenCIConfigAndEnv(configPath);
|
|
1792
|
-
|
|
1793
|
-
try {
|
|
1794
|
-
secret =
|
|
1795
|
-
process.env.SCREENCI_SECRET ??
|
|
1796
|
-
(await ensureScreenciSecret(resolvedConfigPath, opts));
|
|
1797
|
-
}
|
|
1798
|
-
catch (err) {
|
|
1799
|
-
// A waited-out sign-in means recording did not happen: print the (already
|
|
1800
|
-
// formatted) message and exit non-zero so the failure is loud, instead of
|
|
1801
|
-
// looking like a successful run.
|
|
1802
|
-
if (err instanceof SignInTimeoutError) {
|
|
1803
|
-
logger.error(err.message);
|
|
1804
|
-
process.exit(1);
|
|
1805
|
-
}
|
|
1806
|
-
throw err;
|
|
1807
|
-
}
|
|
1761
|
+
const secret = process.env.SCREENCI_SECRET;
|
|
1808
1762
|
if (!secret) {
|
|
1809
|
-
//
|
|
1810
|
-
//
|
|
1811
|
-
// A pending sign-in is an expected handoff (surface the link, then rerun),
|
|
1812
|
-
// not a failure, so exit cleanly (0) to avoid flagging the run as red.
|
|
1813
|
-
if (opts.interactive === false) {
|
|
1814
|
-
process.exit(0);
|
|
1815
|
-
}
|
|
1763
|
+
// No browser sign-in flow: the secret comes from an init one-time token or
|
|
1764
|
+
// from the secrets page. Guide the user to either path and exit non-zero.
|
|
1816
1765
|
const envFilePath = await resolveProjectEnvFilePath(resolvedConfigPath);
|
|
1817
|
-
logger.error(`No SCREENCI_SECRET configured.
|
|
1766
|
+
logger.error(`No SCREENCI_SECRET configured. Copy your secret from ${pc.cyan(getScreenCISecretsUrl())} into ${envFilePath}, or re-run init with a one-time setup token from ${pc.cyan(getScreenCISecretsUrl())}.`);
|
|
1767
|
+
logScreenCISecretGuide();
|
|
1818
1768
|
process.exit(1);
|
|
1819
1769
|
}
|
|
1820
1770
|
return {
|
|
@@ -1825,9 +1775,7 @@ async function requireScreenCISecret(configPath, opts = {}) {
|
|
|
1825
1775
|
};
|
|
1826
1776
|
}
|
|
1827
1777
|
async function updateVideoVisibility(videoId, isPublic, configPath) {
|
|
1828
|
-
const { secret, apiUrl } = await requireScreenCISecret(configPath
|
|
1829
|
-
interactive: detectInteractiveSession(),
|
|
1830
|
-
});
|
|
1778
|
+
const { secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
1831
1779
|
const method = isPublic ? 'PUT' : 'DELETE';
|
|
1832
1780
|
const res = await fetch(`${apiUrl}/cli/public-video/${videoId}`, {
|
|
1833
1781
|
method,
|
|
@@ -1864,7 +1812,7 @@ export function extractGrep(args) {
|
|
|
1864
1812
|
// token stored for the project. An optional `--grep` records only matching
|
|
1865
1813
|
// videos/screenshots.
|
|
1866
1814
|
async function triggerRemoteRun(configPath, grep, languages) {
|
|
1867
|
-
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath
|
|
1815
|
+
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
1868
1816
|
const res = await fetch(`${apiUrl}/cli/trigger-run`, {
|
|
1869
1817
|
method: 'POST',
|
|
1870
1818
|
headers: {
|
|
@@ -1926,9 +1874,7 @@ async function readLastRecordId(screenciDir) {
|
|
|
1926
1874
|
// URLs. Without a local run, only the project-wide listing with `static` URLs is
|
|
1927
1875
|
// returned. The server does the merge; the CLI just passes the recordId.
|
|
1928
1876
|
async function printInfo(configPath) {
|
|
1929
|
-
const { resolvedConfigPath, screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath
|
|
1930
|
-
interactive: detectInteractiveSession(),
|
|
1931
|
-
});
|
|
1877
|
+
const { resolvedConfigPath, screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
1932
1878
|
const screenciDir = resolve(dirname(resolvedConfigPath), '.screenci');
|
|
1933
1879
|
const recordId = await readLastRecordId(screenciDir);
|
|
1934
1880
|
const url = new URL(`${apiUrl}/cli/info`);
|
|
@@ -1945,268 +1891,6 @@ async function printInfo(configPath) {
|
|
|
1945
1891
|
const data = await res.json();
|
|
1946
1892
|
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
1947
1893
|
}
|
|
1948
|
-
async function persistScreenCISecret(envFilePath, secret) {
|
|
1949
|
-
const nextLine = `SCREENCI_SECRET=${secret}`;
|
|
1950
|
-
try {
|
|
1951
|
-
const existing = await readFile(envFilePath, 'utf-8');
|
|
1952
|
-
const lines = existing === '' ? [] : existing.split(/\r?\n/);
|
|
1953
|
-
const firstSecretIndex = lines.findIndex((line) => line.startsWith('SCREENCI_SECRET='));
|
|
1954
|
-
const linesWithoutSecret = lines.filter((line) => !line.startsWith('SCREENCI_SECRET='));
|
|
1955
|
-
const finalLines = firstSecretIndex >= 0
|
|
1956
|
-
? [
|
|
1957
|
-
...linesWithoutSecret.slice(0, firstSecretIndex),
|
|
1958
|
-
nextLine,
|
|
1959
|
-
...linesWithoutSecret.slice(firstSecretIndex),
|
|
1960
|
-
]
|
|
1961
|
-
: [...linesWithoutSecret, nextLine];
|
|
1962
|
-
let nextContent = finalLines.join('\n');
|
|
1963
|
-
if (!nextContent.endsWith('\n'))
|
|
1964
|
-
nextContent += '\n';
|
|
1965
|
-
await writeFile(envFilePath, nextContent);
|
|
1966
|
-
return;
|
|
1967
|
-
}
|
|
1968
|
-
catch (err) {
|
|
1969
|
-
if (!isMissingFileError(err))
|
|
1970
|
-
throw err;
|
|
1971
|
-
}
|
|
1972
|
-
await writeFile(envFilePath, `${nextLine}\n`);
|
|
1973
|
-
}
|
|
1974
|
-
async function pollLinkSessionOnce(spec) {
|
|
1975
|
-
const response = await fetch(spec.pollUrl);
|
|
1976
|
-
const body = (await response.json());
|
|
1977
|
-
const status = body.status ?? 'invalid';
|
|
1978
|
-
if (status === 'completed' && body.secret) {
|
|
1979
|
-
return { status, secret: body.secret };
|
|
1980
|
-
}
|
|
1981
|
-
if (status === 'pending' && new Date().toISOString() >= spec.expiresAt) {
|
|
1982
|
-
return { status: 'expired' };
|
|
1983
|
-
}
|
|
1984
|
-
return { status };
|
|
1985
|
-
}
|
|
1986
|
-
function getAuthPollTimeoutMs(defaultMs) {
|
|
1987
|
-
const raw = process.env.SCREENCI_POLL_AUTH_TIMEOUT_MS;
|
|
1988
|
-
if (raw === undefined)
|
|
1989
|
-
return defaultMs;
|
|
1990
|
-
const parsed = Number(raw);
|
|
1991
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : defaultMs;
|
|
1992
|
-
}
|
|
1993
|
-
async function pollLinkSession(spec, pollIntervalMs = SCREENCI_LINK_SESSION_POLL_INTERVAL_MS, deadlineEpochMs) {
|
|
1994
|
-
for (;;) {
|
|
1995
|
-
const result = await pollLinkSessionOnce(spec);
|
|
1996
|
-
if (result.status === 'completed' && result.secret) {
|
|
1997
|
-
return result;
|
|
1998
|
-
}
|
|
1999
|
-
if (result.status === 'expired' ||
|
|
2000
|
-
result.status === 'consumed' ||
|
|
2001
|
-
result.status === 'invalid') {
|
|
2002
|
-
return result;
|
|
2003
|
-
}
|
|
2004
|
-
// Stop before sleeping again once the optional deadline has passed so
|
|
2005
|
-
// `--poll-auth` cannot block forever waiting for a human to sign in.
|
|
2006
|
-
if (deadlineEpochMs !== undefined && Date.now() >= deadlineEpochMs) {
|
|
2007
|
-
return { status: 'timed-out' };
|
|
2008
|
-
}
|
|
2009
|
-
await new Promise((resolveDelay) => setTimeout(resolveDelay, pollIntervalMs));
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
2013
|
-
const interactive = opts.interactive ?? true;
|
|
2014
|
-
const pollAuth = opts.pollAuth ?? false;
|
|
2015
|
-
const noPollAuth = opts.noPollAuth ?? false;
|
|
2016
|
-
const existingSecret = process.env.SCREENCI_SECRET;
|
|
2017
|
-
if (existingSecret)
|
|
2018
|
-
return existingSecret;
|
|
2019
|
-
try {
|
|
2020
|
-
const environment = getScreenCIEnvironment();
|
|
2021
|
-
const apiUrl = getCliLinkSessionApiUrl();
|
|
2022
|
-
const appUrl = getDevFrontendUrl();
|
|
2023
|
-
const envFilePath = resolvedConfigPath
|
|
2024
|
-
? await resolveProjectEnvFilePath(resolvedConfigPath)
|
|
2025
|
-
: resolve(process.cwd(), '.env');
|
|
2026
|
-
const projectDir = resolvedConfigPath
|
|
2027
|
-
? dirname(resolvedConfigPath)
|
|
2028
|
-
: process.cwd();
|
|
2029
|
-
const specPath = getLinkSessionFilePath(projectDir);
|
|
2030
|
-
const linkSessionContext = {
|
|
2031
|
-
environment,
|
|
2032
|
-
envFilePath,
|
|
2033
|
-
...(resolvedConfigPath ? { resolvedConfigPath } : {}),
|
|
2034
|
-
};
|
|
2035
|
-
const ensureSpec = async () => {
|
|
2036
|
-
const storedSpec = await readPersistedLinkSessionSpec(specPath);
|
|
2037
|
-
const spec = storedSpec &&
|
|
2038
|
-
isStoredLinkSessionReusable(storedSpec, linkSessionContext)
|
|
2039
|
-
? storedSpec
|
|
2040
|
-
: await createLinkSessionSpec({
|
|
2041
|
-
apiUrl,
|
|
2042
|
-
appUrl,
|
|
2043
|
-
...linkSessionContext,
|
|
2044
|
-
});
|
|
2045
|
-
if (spec !== storedSpec) {
|
|
2046
|
-
await writePersistedLinkSessionSpec(specPath, spec);
|
|
2047
|
-
}
|
|
2048
|
-
return spec;
|
|
2049
|
-
};
|
|
2050
|
-
const saveCompletedSecret = async (secret) => {
|
|
2051
|
-
process.env.SCREENCI_SECRET = secret;
|
|
2052
|
-
await persistScreenCISecret(envFilePath, secret);
|
|
2053
|
-
deletePersistedLinkSessionSpec(specPath);
|
|
2054
|
-
logger.info(`Successfully saved SCREENCI_SECRET to ${envFilePath}`);
|
|
2055
|
-
return secret;
|
|
2056
|
-
};
|
|
2057
|
-
// Decide whether this run waits for a browser sign-in or just prints the
|
|
2058
|
-
// link and hands off. A human at a terminal always waits. A plain
|
|
2059
|
-
// non-interactive run (a coding agent: no terminal, no CI) also waits by
|
|
2060
|
-
// default, so `npx screenci record` records without needing a flag. CI and
|
|
2061
|
-
// an explicit SCREENCI_NONINTERACTIVE never wait (CI must not hang and is
|
|
2062
|
-
// expected to provide SCREENCI_SECRET), and `--no-poll-auth` opts back into
|
|
2063
|
-
// that print-and-exit handoff. `--poll-auth` forces waiting and is kept for
|
|
2064
|
-
// backwards compatibility (it is the default for agents now).
|
|
2065
|
-
const isCI = process.env.CI === 'true';
|
|
2066
|
-
const nonInteractiveForced = process.env.SCREENCI_NONINTERACTIVE === '1';
|
|
2067
|
-
const waitForSignIn = noPollAuth
|
|
2068
|
-
? false
|
|
2069
|
-
: interactive || pollAuth || (!isCI && !nonInteractiveForced);
|
|
2070
|
-
const pollIntervalMs = interactive
|
|
2071
|
-
? SCREENCI_LINK_SESSION_POLL_INTERVAL_MS
|
|
2072
|
-
: SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS;
|
|
2073
|
-
const timeoutMs = getAuthPollTimeoutMs(interactive
|
|
2074
|
-
? SCREENCI_LINK_SESSION_INTERACTIVE_TIMEOUT_MS
|
|
2075
|
-
: SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS);
|
|
2076
|
-
// Check the current/stored session once. If sign-in already completed (for
|
|
2077
|
-
// example between runs) we pick up the secret immediately. Recreate a stale
|
|
2078
|
-
// session and check once more before deciding to wait or hand off.
|
|
2079
|
-
let spec = await ensureSpec();
|
|
2080
|
-
let result = await pollLinkSessionOnce(spec);
|
|
2081
|
-
if (result.status === 'expired' ||
|
|
2082
|
-
result.status === 'consumed' ||
|
|
2083
|
-
result.status === 'invalid') {
|
|
2084
|
-
deletePersistedLinkSessionSpec(specPath);
|
|
2085
|
-
spec = await ensureSpec();
|
|
2086
|
-
result = await pollLinkSessionOnce(spec);
|
|
2087
|
-
}
|
|
2088
|
-
if (result.status === 'completed' && result.secret) {
|
|
2089
|
-
return await saveCompletedSecret(result.secret);
|
|
2090
|
-
}
|
|
2091
|
-
if (!waitForSignIn) {
|
|
2092
|
-
// CI / SCREENCI_NONINTERACTIVE / --no-poll-auth: print the link and hand
|
|
2093
|
-
// off without waiting. The caller exits cleanly (0); a pending sign-in
|
|
2094
|
-
// here is an expected handoff, not a failure.
|
|
2095
|
-
logger.info(`Sign-in required to record. Open this link to sign in:\n${pc.cyan(spec.appUrl)}\n` +
|
|
2096
|
-
`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.`);
|
|
2097
|
-
return undefined;
|
|
2098
|
-
}
|
|
2099
|
-
// Wait for sign-in: print the link, then poll until the human finishes and
|
|
2100
|
-
// continue recording automatically once the secret lands. We re-print the
|
|
2101
|
-
// link on every (re)created session so the latest valid link is always
|
|
2102
|
-
// visible. We do not wait forever: after the timeout we throw so the caller
|
|
2103
|
-
// exits non-zero (a timed-out sign-in means nothing was recorded). The link
|
|
2104
|
-
// stays valid for a rerun.
|
|
2105
|
-
const timeoutMinutes = Math.max(1, Math.round(timeoutMs / 60_000));
|
|
2106
|
-
const intervalSeconds = Math.max(1, Math.round(pollIntervalMs / 1_000));
|
|
2107
|
-
const deadlineEpochMs = Date.now() + timeoutMs;
|
|
2108
|
-
for (;;) {
|
|
2109
|
-
logger.info(`Sign-in required to record. Open this link to sign in:\n${pc.cyan(spec.appUrl)}\n` +
|
|
2110
|
-
`Waiting for sign-in (checking every ${intervalSeconds} seconds, up to ${timeoutMinutes} minutes). Recording continues automatically once you finish.`);
|
|
2111
|
-
const polled = await pollLinkSession(spec, pollIntervalMs, deadlineEpochMs);
|
|
2112
|
-
if (polled.status === 'completed' && polled.secret) {
|
|
2113
|
-
return await saveCompletedSecret(polled.secret);
|
|
2114
|
-
}
|
|
2115
|
-
if (polled.status === 'timed-out' || Date.now() >= deadlineEpochMs) {
|
|
2116
|
-
throw new SignInTimeoutError(`Timed out after ${timeoutMinutes} minutes waiting for sign-in. The link is still valid:\n${pc.cyan(spec.appUrl)}\n` +
|
|
2117
|
-
`After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to record.`);
|
|
2118
|
-
}
|
|
2119
|
-
// Session went stale (expired/consumed/invalid) before sign-in: recreate
|
|
2120
|
-
// and keep waiting until the deadline.
|
|
2121
|
-
deletePersistedLinkSessionSpec(specPath);
|
|
2122
|
-
spec = await ensureSpec();
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
catch (err) {
|
|
2126
|
-
// A timed-out sign-in is a deliberate, already-formatted failure: let it
|
|
2127
|
-
// propagate so the caller can exit non-zero rather than swallowing it as a
|
|
2128
|
-
// generic auth error.
|
|
2129
|
-
if (err instanceof SignInTimeoutError)
|
|
2130
|
-
throw err;
|
|
2131
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2132
|
-
logger.warn(`Authentication failed: ${msg}`);
|
|
2133
|
-
logger.info(`You can add SCREENCI_SECRET manually to ${resolvedConfigPath ? await resolveProjectEnvFilePath(resolvedConfigPath) : '.env'} later. Get it from ${getScreenCISecretsUrl()}.`);
|
|
2134
|
-
logScreenCISecretGuide();
|
|
2135
|
-
return undefined;
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
/**
|
|
2139
|
-
* `screenci login`: get a sign-in link in front of a human as early as
|
|
2140
|
-
* possible. It reuses the link `init` (or a prior run) persisted, or creates a
|
|
2141
|
-
* fresh one, tries to open it in the browser best-effort, and always prints it
|
|
2142
|
-
* as a fallback. It does NOT wait for sign-in to finish: the agent can run this,
|
|
2143
|
-
* keep authoring and `test`ing, then `record` (which blocks for sign-in) once
|
|
2144
|
-
* the script is green. If sign-in already completed, it saves the secret and
|
|
2145
|
-
* exits.
|
|
2146
|
-
*/
|
|
2147
|
-
export async function runLogin(configPath) {
|
|
2148
|
-
const { resolvedConfigPath } = await loadScreenCIConfigAndEnv(configPath);
|
|
2149
|
-
if (process.env.SCREENCI_SECRET) {
|
|
2150
|
-
logger.info(`Already signed in (SCREENCI_SECRET is set). Run ${pc.cyan(getSuggestedScreenciCommand('record'))} to record.`);
|
|
2151
|
-
return;
|
|
2152
|
-
}
|
|
2153
|
-
const environment = getScreenCIEnvironment();
|
|
2154
|
-
const apiUrl = getCliLinkSessionApiUrl();
|
|
2155
|
-
const appUrl = getDevFrontendUrl();
|
|
2156
|
-
const envFilePath = await resolveProjectEnvFilePath(resolvedConfigPath);
|
|
2157
|
-
const projectDir = dirname(resolvedConfigPath);
|
|
2158
|
-
const specPath = getLinkSessionFilePath(projectDir);
|
|
2159
|
-
const linkSessionContext = { environment, envFilePath, resolvedConfigPath };
|
|
2160
|
-
const createSpec = async () => {
|
|
2161
|
-
const created = await createLinkSessionSpec({
|
|
2162
|
-
apiUrl,
|
|
2163
|
-
appUrl,
|
|
2164
|
-
...linkSessionContext,
|
|
2165
|
-
});
|
|
2166
|
-
await writePersistedLinkSessionSpec(specPath, created);
|
|
2167
|
-
return created;
|
|
2168
|
-
};
|
|
2169
|
-
try {
|
|
2170
|
-
// Reuse a still-valid persisted session so login and a later record share
|
|
2171
|
-
// the same link; otherwise create and persist a fresh one.
|
|
2172
|
-
const stored = await readPersistedLinkSessionSpec(specPath);
|
|
2173
|
-
let spec = stored && isStoredLinkSessionReusable(stored, linkSessionContext)
|
|
2174
|
-
? stored
|
|
2175
|
-
: await createSpec();
|
|
2176
|
-
// If sign-in already completed (e.g. between runs) save the secret and stop:
|
|
2177
|
-
// there is nothing left to open. Recreate a stale session before opening so
|
|
2178
|
-
// the link we hand off is valid.
|
|
2179
|
-
const result = await pollLinkSessionOnce(spec);
|
|
2180
|
-
if (result.status === 'completed' && result.secret) {
|
|
2181
|
-
process.env.SCREENCI_SECRET = result.secret;
|
|
2182
|
-
await persistScreenCISecret(envFilePath, result.secret);
|
|
2183
|
-
deletePersistedLinkSessionSpec(specPath);
|
|
2184
|
-
logger.info(`Already signed in. Saved SCREENCI_SECRET to ${envFilePath}.`);
|
|
2185
|
-
return;
|
|
2186
|
-
}
|
|
2187
|
-
if (result.status === 'expired' ||
|
|
2188
|
-
result.status === 'consumed' ||
|
|
2189
|
-
result.status === 'invalid') {
|
|
2190
|
-
deletePersistedLinkSessionSpec(specPath);
|
|
2191
|
-
spec = await createSpec();
|
|
2192
|
-
}
|
|
2193
|
-
const opened = await openUrlInBrowser(spec.appUrl);
|
|
2194
|
-
if (opened.opened) {
|
|
2195
|
-
logger.info(`Opening the sign-in link in your browser:\n${pc.cyan(spec.appUrl)}`);
|
|
2196
|
-
}
|
|
2197
|
-
else {
|
|
2198
|
-
logger.info(`Could not open a browser automatically (${opened.reason}). Open this link to sign in:\n${pc.cyan(spec.appUrl)}`);
|
|
2199
|
-
}
|
|
2200
|
-
logger.info(`The link stays valid for 24 hours. You can keep authoring and run ${pc.cyan(getSuggestedScreenciCommand('test'))} while you sign in. Then run ${pc.cyan(getSuggestedScreenciCommand('record'))}, which waits for sign-in and records once you finish.`);
|
|
2201
|
-
}
|
|
2202
|
-
catch (err) {
|
|
2203
|
-
// Login is a best-effort convenience: never fail the run on a network or
|
|
2204
|
-
// browser hiccup. record can still create and wait on the link later.
|
|
2205
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2206
|
-
logger.warn(`Could not start sign-in: ${msg}`);
|
|
2207
|
-
logger.info(`You can sign in later: ${pc.cyan(getSuggestedScreenciCommand('record'))} prints a link and waits for sign-in. In CI, set SCREENCI_SECRET (from ${getScreenCISecretsUrl()}).`);
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
1894
|
// Uploads the recordings already written under `.screenci` for the resolved
|
|
2211
1895
|
// config. Shared by `record` (after a Playwright run) and `retry` (which
|
|
2212
1896
|
// re-sends the existing recordings without re-running Playwright). A non-null
|
|
@@ -2236,7 +1920,7 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2236
1920
|
logger.info('All recordings failed.');
|
|
2237
1921
|
}
|
|
2238
1922
|
else if (!secret) {
|
|
2239
|
-
logger.info(`No SCREENCI_SECRET configured for uploads.
|
|
1923
|
+
logger.info(`No SCREENCI_SECRET configured for uploads. Copy your secret from ${getScreenCISecretsUrl()} into the project env file, then rerun ${getSuggestedScreenciCommand('record')}.`);
|
|
2240
1924
|
}
|
|
2241
1925
|
else if (playwrightFailure !== null &&
|
|
2242
1926
|
uploadPolicy === 'all-or-nothing') {
|
|
@@ -2309,7 +1993,12 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2309
1993
|
}
|
|
2310
1994
|
if (projectId !== null && plan !== 'business') {
|
|
2311
1995
|
logger.info('');
|
|
2312
|
-
|
|
1996
|
+
if (plan === 'free') {
|
|
1997
|
+
logger.info('You are on the free tier, so this render includes a ScreenCI watermark. Upgrade to remove it and get more renders, more active videos, and expressive narration:');
|
|
1998
|
+
}
|
|
1999
|
+
else {
|
|
2000
|
+
logger.info('Upgrade for more renders, more active videos, and expressive narration:');
|
|
2001
|
+
}
|
|
2313
2002
|
logger.info(pc.cyan(`${appUrl}/select-plan`));
|
|
2314
2003
|
}
|
|
2315
2004
|
for (const notice of studioNotices) {
|
|
@@ -2348,7 +2037,7 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2348
2037
|
export async function main() {
|
|
2349
2038
|
if (process.argv.length <= 2) {
|
|
2350
2039
|
logger.error('Error: No command provided');
|
|
2351
|
-
logger.error('Available commands: record, test,
|
|
2040
|
+
logger.error('Available commands: record, test, info, make-public, make-private, init');
|
|
2352
2041
|
process.exit(1);
|
|
2353
2042
|
}
|
|
2354
2043
|
const program = new Command();
|
|
@@ -2360,8 +2049,6 @@ export async function main() {
|
|
|
2360
2049
|
.command('record [playwrightArgs...]')
|
|
2361
2050
|
.description('Record videos using Playwright')
|
|
2362
2051
|
.option('-v, --verbose', 'verbose output')
|
|
2363
|
-
.option('--poll-auth', 'wait for sign-in before recording (now the default off-CI; kept for backwards compatibility)')
|
|
2364
|
-
.option('--no-poll-auth', 'do not wait for sign-in: print the link and exit cleanly if not signed in (the default under CI)')
|
|
2365
2052
|
.option('--remote', 'trigger the GitHub Actions recording workflow for this project remotely instead of recording locally')
|
|
2366
2053
|
.option('--languages <langs>', 'record/render only these languages (comma-separated, e.g. fi,en)')
|
|
2367
2054
|
.allowUnknownOption(true)
|
|
@@ -2383,7 +2070,7 @@ export async function main() {
|
|
|
2383
2070
|
const uploadExisting = isUploadExistingEnabled();
|
|
2384
2071
|
if (!uploadExisting) {
|
|
2385
2072
|
try {
|
|
2386
|
-
await run('record', parsed.otherArgs, parsed.configPath, parsed.verbose, false, parsed.
|
|
2073
|
+
await run('record', parsed.otherArgs, parsed.configPath, parsed.verbose, false, parsed.languages);
|
|
2387
2074
|
}
|
|
2388
2075
|
catch (error) {
|
|
2389
2076
|
if (!(error instanceof Error))
|
|
@@ -2438,13 +2125,6 @@ export async function main() {
|
|
|
2438
2125
|
const recordCommand = getSuggestedScreenciCommand('record');
|
|
2439
2126
|
logger.info(`Tests passed. Run ${pc.cyan(recordCommand)} to render the videos.`);
|
|
2440
2127
|
});
|
|
2441
|
-
program
|
|
2442
|
-
.command('login')
|
|
2443
|
-
.description('Open the sign-in link in your browser (best-effort) so recording can finish later')
|
|
2444
|
-
.option('-c, --config <path>', 'path to screenci.config.ts')
|
|
2445
|
-
.action(async (options) => {
|
|
2446
|
-
await runLogin(options['config']);
|
|
2447
|
-
});
|
|
2448
2128
|
program
|
|
2449
2129
|
.command('info')
|
|
2450
2130
|
.description("Print the latest record run's video URLs and render status as JSON")
|
|
@@ -2522,8 +2202,6 @@ function getSubcommandArgv(command) {
|
|
|
2522
2202
|
export function parseRecordCliArgs(args) {
|
|
2523
2203
|
let configPath;
|
|
2524
2204
|
let verbose = false;
|
|
2525
|
-
let pollAuth = false;
|
|
2526
|
-
let noPollAuth = false;
|
|
2527
2205
|
let remote = false;
|
|
2528
2206
|
let languages;
|
|
2529
2207
|
const otherArgs = [];
|
|
@@ -2557,12 +2235,6 @@ export function parseRecordCliArgs(args) {
|
|
|
2557
2235
|
else if (arg === '--verbose' || arg === '-v') {
|
|
2558
2236
|
verbose = true;
|
|
2559
2237
|
}
|
|
2560
|
-
else if (arg === '--poll-auth') {
|
|
2561
|
-
pollAuth = true;
|
|
2562
|
-
}
|
|
2563
|
-
else if (arg === '--no-poll-auth') {
|
|
2564
|
-
noPollAuth = true;
|
|
2565
|
-
}
|
|
2566
2238
|
else if (arg === '--remote') {
|
|
2567
2239
|
remote = true;
|
|
2568
2240
|
}
|
|
@@ -2573,8 +2245,6 @@ export function parseRecordCliArgs(args) {
|
|
|
2573
2245
|
return {
|
|
2574
2246
|
configPath,
|
|
2575
2247
|
verbose,
|
|
2576
|
-
pollAuth,
|
|
2577
|
-
noPollAuth,
|
|
2578
2248
|
remote,
|
|
2579
2249
|
languages,
|
|
2580
2250
|
otherArgs,
|
|
@@ -2637,7 +2307,7 @@ function validateArgs(args) {
|
|
|
2637
2307
|
*/
|
|
2638
2308
|
async function fetchTextOverridesEnv(configPath, languages, verbose) {
|
|
2639
2309
|
try {
|
|
2640
|
-
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath
|
|
2310
|
+
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
2641
2311
|
const params = new URLSearchParams({
|
|
2642
2312
|
projectName: screenciConfig.projectName,
|
|
2643
2313
|
});
|
|
@@ -2677,7 +2347,7 @@ async function fetchTextOverridesEnv(configPath, languages, verbose) {
|
|
|
2677
2347
|
*/
|
|
2678
2348
|
async function fetchRecordOptionsEnv(configPath, verbose) {
|
|
2679
2349
|
try {
|
|
2680
|
-
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath
|
|
2350
|
+
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
2681
2351
|
const params = new URLSearchParams({
|
|
2682
2352
|
projectName: screenciConfig.projectName,
|
|
2683
2353
|
});
|
|
@@ -2705,20 +2375,14 @@ async function fetchRecordOptionsEnv(configPath, verbose) {
|
|
|
2705
2375
|
return {};
|
|
2706
2376
|
}
|
|
2707
2377
|
}
|
|
2708
|
-
async function run(command, additionalArgs, customConfigPath, verbose = false, mockRecord = false,
|
|
2378
|
+
async function run(command, additionalArgs, customConfigPath, verbose = false, mockRecord = false, languages) {
|
|
2709
2379
|
const configPath = resolveScreenCIConfigPathOrExit(customConfigPath);
|
|
2710
2380
|
if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
|
|
2711
2381
|
await loadEnvFileFromConfigSource(configPath, false);
|
|
2712
2382
|
}
|
|
2713
2383
|
// Only validate args for record command
|
|
2714
2384
|
if (command === 'record') {
|
|
2715
|
-
|
|
2716
|
-
await requireScreenCISecret(configPath, {
|
|
2717
|
-
interactive: detectInteractiveSession(),
|
|
2718
|
-
pollAuth,
|
|
2719
|
-
noPollAuth,
|
|
2720
|
-
});
|
|
2721
|
-
}
|
|
2385
|
+
await requireScreenCISecret(configPath);
|
|
2722
2386
|
validateArgs(additionalArgs);
|
|
2723
2387
|
const screenciDir = resolve(dirname(configPath), '.screenci');
|
|
2724
2388
|
clearRecordingDirectories(screenciDir);
|