screenci 0.0.63 → 0.0.65
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 +25 -17
- package/dist/cli.d.ts +17 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +179 -55
- package/dist/cli.js.map +1 -1
- package/dist/docs/manifest.d.ts +92 -61
- package/dist/docs/manifest.d.ts.map +1 -1
- package/dist/docs/manifest.js +32 -21
- package/dist/docs/manifest.js.map +1 -1
- package/dist/docs/videos.d.ts +1 -1
- package/dist/docs/videos.js +1 -1
- package/dist/docs/videos.js.map +1 -1
- package/dist/src/init.d.ts +9 -0
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +297 -109
- package/dist/src/init.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/skills/screenci/SKILL.md +5 -3
- package/skills/screenci/references/record.md +2 -1
package/README.md
CHANGED
|
@@ -15,23 +15,27 @@ npm init screenci@latest
|
|
|
15
15
|
pnpm create screenci
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`init` creates a ready-to-run
|
|
19
|
-
dependencies, and installs Chromium by default.
|
|
18
|
+
`init` creates a ready-to-run, self-contained `screenci/` directory ("the
|
|
19
|
+
island") with its own dependencies, and installs Chromium by default. The
|
|
20
|
+
island is deliberately isolated from any surrounding workspace, which makes
|
|
21
|
+
installation reliable inside complex monorepos. When using `npm init`, pass
|
|
20
22
|
extra initializer flags after `--`, for example
|
|
21
23
|
`npm init screenci@latest -- --yes --package-manager pnpm`.
|
|
22
24
|
|
|
23
25
|
```text
|
|
24
|
-
screenci
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.gitignore
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.github/workflows/screenci.yaml
|
|
26
|
+
screenci/
|
|
27
|
+
screenci.config.ts
|
|
28
|
+
package.json
|
|
29
|
+
package-lock.json # or pnpm-lock.yaml / yarn.lock
|
|
30
|
+
.gitignore
|
|
31
|
+
videos/
|
|
32
|
+
example.video.ts
|
|
33
|
+
.github/workflows/screenci.yaml # at the repo root, scoped to screenci/
|
|
33
34
|
```
|
|
34
35
|
|
|
36
|
+
`screenci test` and `screenci record` work both from inside `screenci/` and
|
|
37
|
+
from the repository root.
|
|
38
|
+
|
|
35
39
|
Docs:
|
|
36
40
|
|
|
37
41
|
- Getting started: `https://screenci.com/docs`
|
|
@@ -92,12 +96,16 @@ Playwright without starting the final recording and upload path.
|
|
|
92
96
|
npx screenci record
|
|
93
97
|
```
|
|
94
98
|
|
|
95
|
-
On the first run without `SCREENCI_SECRET
|
|
96
|
-
link, waits for you to finish sign-in in the browser,
|
|
97
|
-
project env file, and then continues.
|
|
98
|
-
|
|
99
|
-
it
|
|
100
|
-
|
|
99
|
+
On the first run without `SCREENCI_SECRET` in an interactive terminal, `record`
|
|
100
|
+
prints a one-time ScreenCI link, waits for you to finish sign-in in the browser,
|
|
101
|
+
saves the secret into the project env file, and then continues. In a
|
|
102
|
+
non-interactive session (no terminal, or `SCREENCI_NONINTERACTIVE=1`) `record`
|
|
103
|
+
does not wait: it prints the sign-in link and exits, so you can open the link,
|
|
104
|
+
sign in, choose a plan, and rerun `record`, which then detects the completed
|
|
105
|
+
session and continues. Set `SCREENCI_SECRET` ahead of time to skip sign-in
|
|
106
|
+
entirely. Pending auth state is cached in `.screenci/link-session.json`, so
|
|
107
|
+
rerunning `record` reuses the same link until it expires or completes. Recorded
|
|
108
|
+
artifacts still live in `.screenci/<video-name>/`.
|
|
101
109
|
|
|
102
110
|
## Configure
|
|
103
111
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -8,6 +8,20 @@ type PlaywrightListReportSuite = {
|
|
|
8
8
|
}>;
|
|
9
9
|
suites?: PlaywrightListReportSuite[];
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Reports whether the current session can complete an interactive browser
|
|
13
|
+
* sign-in. A session is interactive only when both stdin and stdout are
|
|
14
|
+
* attached to a terminal and no signal marks the run as automated. This is the
|
|
15
|
+
* proxy for "a human is present to open the sign-in link" — it does not attempt
|
|
16
|
+
* to identify any particular caller (CI, a piped shell, or an automated tool).
|
|
17
|
+
*
|
|
18
|
+
* Dependency-injected so tests can force a value without a real terminal.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectInteractiveSession(env?: NodeJS.ProcessEnv, stdout?: {
|
|
21
|
+
isTTY?: boolean;
|
|
22
|
+
}, stdin?: {
|
|
23
|
+
isTTY?: boolean;
|
|
24
|
+
}): boolean;
|
|
11
25
|
export declare function collectPlaywrightListTitles(suites: readonly PlaywrightListReportSuite[]): string[];
|
|
12
26
|
type PreparedUploadAsset = {
|
|
13
27
|
fileHash: string;
|
|
@@ -66,7 +80,9 @@ export declare function extractConfigStringLiteral(configSource: string, propert
|
|
|
66
80
|
export declare function extractRecordUploadPolicyLiteral(configSource: string): RecordUploadPolicy | undefined;
|
|
67
81
|
export declare function extractMockRecordLiteral(configSource: string): boolean | undefined;
|
|
68
82
|
export declare function getConfigModuleSpecifier(resolvedConfigPath: string): string;
|
|
69
|
-
export declare function ensureScreenciSecret(resolvedConfigPath?: string
|
|
83
|
+
export declare function ensureScreenciSecret(resolvedConfigPath?: string, opts?: {
|
|
84
|
+
interactive?: boolean;
|
|
85
|
+
}): Promise<string | undefined>;
|
|
70
86
|
export declare function main(): Promise<void>;
|
|
71
87
|
export declare function logCliError(error: unknown): void;
|
|
72
88
|
export {};
|
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;AAoCxE,KAAK,yBAAyB,GAAG;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAChC,MAAM,CAAC,EAAE,yBAAyB,EAAE,CAAA;CACrC,CAAA;AAmDD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,SAAS,yBAAyB,EAAE,GAC3C,MAAM,EAAE,CAiBV;AA6MD,KAAK,mBAAmB,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AASD,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACd;IAAE,cAAc,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAA;AAE7C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,gBAAgB,CAAA;CACzB,CAAA;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,mBAAmB,EAAE,GAC7B,MAAM,CAcR;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,MAAM,CAER;AAmWD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,GAAG,KAAK,GAAG,OAAO,CAAC,EACtD,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI,GACxC,MAAM,IAAI,CAiBZ;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -36,6 +36,22 @@ function getScreenCIEnvironment() {
|
|
|
36
36
|
const parsed = parseScreenCIEnvironment(process.env[SCREENCI_ENVIRONMENT_VARIABLE]);
|
|
37
37
|
return parsed ?? 'prod';
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Reports whether the current session can complete an interactive browser
|
|
41
|
+
* sign-in. A session is interactive only when both stdin and stdout are
|
|
42
|
+
* attached to a terminal and no signal marks the run as automated. This is the
|
|
43
|
+
* proxy for "a human is present to open the sign-in link" — it does not attempt
|
|
44
|
+
* to identify any particular caller (CI, a piped shell, or an automated tool).
|
|
45
|
+
*
|
|
46
|
+
* Dependency-injected so tests can force a value without a real terminal.
|
|
47
|
+
*/
|
|
48
|
+
export function detectInteractiveSession(env = process.env, stdout = process.stdout, stdin = process.stdin) {
|
|
49
|
+
if (env.SCREENCI_NONINTERACTIVE === '1')
|
|
50
|
+
return false;
|
|
51
|
+
if (env.CI === 'true')
|
|
52
|
+
return false;
|
|
53
|
+
return Boolean(stdout.isTTY) && Boolean(stdin.isTTY);
|
|
54
|
+
}
|
|
39
55
|
export function collectPlaywrightListTitles(suites) {
|
|
40
56
|
const titles = [];
|
|
41
57
|
const visitSuite = (suite) => {
|
|
@@ -575,10 +591,27 @@ function resolveWindowsCmdShim(cmd) {
|
|
|
575
591
|
}
|
|
576
592
|
return shimName;
|
|
577
593
|
}
|
|
594
|
+
function isModuleNotFoundError(error) {
|
|
595
|
+
return (error instanceof Error &&
|
|
596
|
+
error.code === 'MODULE_NOT_FOUND');
|
|
597
|
+
}
|
|
598
|
+
function resolvePlaywrightCliEntrypoint(searchFrom) {
|
|
599
|
+
// Prefer the @playwright/test installed alongside the user's config, since it
|
|
600
|
+
// is declared as a peer dependency of the project being recorded.
|
|
601
|
+
try {
|
|
602
|
+
return require.resolve('@playwright/test/cli', { paths: [searchFrom] });
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
if (!isModuleNotFoundError(error))
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
// Fall back to the copy resolvable from the screenci CLI's own install. This
|
|
609
|
+
// keeps discovery working when Playwright is hoisted to a parent install or
|
|
610
|
+
// bundled with the CLI rather than next to the config file.
|
|
611
|
+
return require.resolve('@playwright/test/cli');
|
|
612
|
+
}
|
|
578
613
|
function resolvePlaywrightSpawnSpec(args, searchFrom) {
|
|
579
|
-
const cliEntrypoint =
|
|
580
|
-
paths: [searchFrom],
|
|
581
|
-
});
|
|
614
|
+
const cliEntrypoint = resolvePlaywrightCliEntrypoint(searchFrom);
|
|
582
615
|
return {
|
|
583
616
|
command: process.execPath,
|
|
584
617
|
args: [cliEntrypoint, ...args],
|
|
@@ -662,17 +695,61 @@ function clearRecordingDirectories(dir) {
|
|
|
662
695
|
function findScreenCIConfig(customPath) {
|
|
663
696
|
if (customPath) {
|
|
664
697
|
const resolvedPath = resolve(process.cwd(), customPath);
|
|
665
|
-
|
|
666
|
-
|
|
698
|
+
return existsSync(resolvedPath)
|
|
699
|
+
? { kind: 'found', path: resolvedPath }
|
|
700
|
+
: { kind: 'not-found' };
|
|
701
|
+
}
|
|
702
|
+
// Walk up from the current directory looking for a flat `screenci.config.ts`,
|
|
703
|
+
// which is what's present when the command runs from inside the `screenci/`
|
|
704
|
+
// island. We deliberately do NOT auto-use a nested
|
|
705
|
+
// `screenci/screenci.config.ts`: running the CLI from outside the island
|
|
706
|
+
// resolves the `screenci` binary from the registry (npx download) rather than
|
|
707
|
+
// the version-pinned island install, so it would silently run a different
|
|
708
|
+
// version. Instead we detect the island and ask the user to `cd` into it.
|
|
709
|
+
let current = process.cwd();
|
|
710
|
+
let islandConfigPath;
|
|
711
|
+
while (true) {
|
|
712
|
+
const flatConfig = resolve(current, 'screenci.config.ts');
|
|
713
|
+
if (existsSync(flatConfig)) {
|
|
714
|
+
return { kind: 'found', path: flatConfig };
|
|
715
|
+
}
|
|
716
|
+
if (islandConfigPath === undefined) {
|
|
717
|
+
const islandConfig = resolve(current, 'screenci', 'screenci.config.ts');
|
|
718
|
+
if (existsSync(islandConfig)) {
|
|
719
|
+
islandConfigPath = islandConfig;
|
|
720
|
+
}
|
|
667
721
|
}
|
|
668
|
-
|
|
722
|
+
const parent = dirname(current);
|
|
723
|
+
if (parent === current)
|
|
724
|
+
break;
|
|
725
|
+
current = parent;
|
|
669
726
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
727
|
+
if (islandConfigPath !== undefined) {
|
|
728
|
+
return { kind: 'island-not-entered', islandConfigPath };
|
|
729
|
+
}
|
|
730
|
+
return { kind: 'not-found' };
|
|
731
|
+
}
|
|
732
|
+
// Resolve the config path, or log a helpful message and exit. Centralizes the
|
|
733
|
+
// `cd screenci` guidance so every command (test/record/info/...) behaves the
|
|
734
|
+
// same when invoked from outside the island.
|
|
735
|
+
function resolveScreenCIConfigPathOrExit(customPath) {
|
|
736
|
+
const resolution = findScreenCIConfig(customPath);
|
|
737
|
+
switch (resolution.kind) {
|
|
738
|
+
case 'found':
|
|
739
|
+
return resolution.path;
|
|
740
|
+
case 'island-not-entered': {
|
|
741
|
+
const islandDir = dirname(resolution.islandConfigPath);
|
|
742
|
+
const relDir = pathRelative(process.cwd(), islandDir) || '.';
|
|
743
|
+
logger.error(`Error: no screenci.config.ts found here, but found ${pc.cyan(`${relDir}/screenci.config.ts`)}. Run ${pc.cyan(`cd ${relDir}`)} and rerun the command from there.`);
|
|
744
|
+
return process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
case 'not-found': {
|
|
747
|
+
logger.error(customPath
|
|
748
|
+
? `Error: Config file not found: ${customPath}`
|
|
749
|
+
: 'Error: screenci.config.ts not found in the current directory or any parent.');
|
|
750
|
+
return process.exit(1);
|
|
751
|
+
}
|
|
674
752
|
}
|
|
675
|
-
return null;
|
|
676
753
|
}
|
|
677
754
|
async function hashFile(filePath) {
|
|
678
755
|
return new Promise((resolveHash, reject) => {
|
|
@@ -1123,7 +1200,13 @@ export function getDevFrontendUrl() {
|
|
|
1123
1200
|
}
|
|
1124
1201
|
}
|
|
1125
1202
|
export function getCliLinkSessionApiUrl() {
|
|
1126
|
-
|
|
1203
|
+
// The `/cli-link/session` routes are Convex HTTP actions served from the same
|
|
1204
|
+
// backend host as the `/cli/*` upload endpoints. We hit that host directly
|
|
1205
|
+
// rather than the frontend: the frontend only forwards these routes via the
|
|
1206
|
+
// vite dev-server proxy, which does not exist on the hosted dev/prod frontend
|
|
1207
|
+
// (a POST there returns 405). The CLI runs under Node, so there is no CORS
|
|
1208
|
+
// constraint that would require going through the frontend origin.
|
|
1209
|
+
return getDevBackendUrl();
|
|
1127
1210
|
}
|
|
1128
1211
|
function getScreenCISecretsUrl() {
|
|
1129
1212
|
return `${getDevFrontendUrl()}/secrets`;
|
|
@@ -1135,14 +1218,7 @@ async function writeGitHubProjectOutput(projectUrl) {
|
|
|
1135
1218
|
await appendFile(githubOutput, `screenci_project_url=${projectUrl}\n`);
|
|
1136
1219
|
}
|
|
1137
1220
|
async function loadScreenCIConfigAndEnv(configPath) {
|
|
1138
|
-
const resolvedConfigPath =
|
|
1139
|
-
if (!resolvedConfigPath) {
|
|
1140
|
-
const errorMsg = configPath
|
|
1141
|
-
? `Error: Config file not found: ${configPath}`
|
|
1142
|
-
: 'Error: screenci.config.ts not found in current directory';
|
|
1143
|
-
logger.error(errorMsg);
|
|
1144
|
-
process.exit(1);
|
|
1145
|
-
}
|
|
1221
|
+
const resolvedConfigPath = resolveScreenCIConfigPathOrExit(configPath);
|
|
1146
1222
|
let screenciConfig;
|
|
1147
1223
|
try {
|
|
1148
1224
|
screenciConfig =
|
|
@@ -1334,11 +1410,16 @@ async function loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath) {
|
|
|
1334
1410
|
throw err;
|
|
1335
1411
|
}
|
|
1336
1412
|
}
|
|
1337
|
-
async function requireScreenCISecret(configPath) {
|
|
1413
|
+
async function requireScreenCISecret(configPath, opts = {}) {
|
|
1338
1414
|
const { resolvedConfigPath, screenciConfig } = await loadScreenCIConfigAndEnv(configPath);
|
|
1339
1415
|
const secret = process.env.SCREENCI_SECRET ??
|
|
1340
|
-
(await ensureScreenciSecret(resolvedConfigPath));
|
|
1416
|
+
(await ensureScreenciSecret(resolvedConfigPath, opts));
|
|
1341
1417
|
if (!secret) {
|
|
1418
|
+
// In a non-interactive session ensureScreenciSecret already printed the
|
|
1419
|
+
// sign-in link and the next step, so we exit without repeating guidance.
|
|
1420
|
+
if (opts.interactive === false) {
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1342
1423
|
const envFilePath = await resolveProjectEnvFilePath(resolvedConfigPath);
|
|
1343
1424
|
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)}.`);
|
|
1344
1425
|
process.exit(1);
|
|
@@ -1351,7 +1432,7 @@ async function requireScreenCISecret(configPath) {
|
|
|
1351
1432
|
};
|
|
1352
1433
|
}
|
|
1353
1434
|
async function fetchProjectInfo(configPath) {
|
|
1354
|
-
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
1435
|
+
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath, { interactive: detectInteractiveSession() });
|
|
1355
1436
|
const url = new URL(`${apiUrl}/cli/project-info`);
|
|
1356
1437
|
url.searchParams.set('projectName', screenciConfig.projectName);
|
|
1357
1438
|
const res = await fetch(url.toString(), {
|
|
@@ -1370,7 +1451,9 @@ async function printProjectInfo(configPath) {
|
|
|
1370
1451
|
process.stdout.write(`${JSON.stringify(info, null, 2)}\n`);
|
|
1371
1452
|
}
|
|
1372
1453
|
async function updateVideoVisibility(videoId, isPublic, configPath) {
|
|
1373
|
-
const { secret, apiUrl } = await requireScreenCISecret(configPath
|
|
1454
|
+
const { secret, apiUrl } = await requireScreenCISecret(configPath, {
|
|
1455
|
+
interactive: detectInteractiveSession(),
|
|
1456
|
+
});
|
|
1374
1457
|
const method = isPublic ? 'PUT' : 'DELETE';
|
|
1375
1458
|
const res = await fetch(`${apiUrl}/cli/public-video/${videoId}`, {
|
|
1376
1459
|
method,
|
|
@@ -1461,24 +1544,34 @@ async function createLinkSessionSpec(options) {
|
|
|
1461
1544
|
envFilePath: options.envFilePath,
|
|
1462
1545
|
};
|
|
1463
1546
|
}
|
|
1547
|
+
async function pollLinkSessionOnce(spec) {
|
|
1548
|
+
const response = await fetch(spec.pollUrl);
|
|
1549
|
+
const body = (await response.json());
|
|
1550
|
+
const status = body.status ?? 'invalid';
|
|
1551
|
+
if (status === 'completed' && body.secret) {
|
|
1552
|
+
return { status, secret: body.secret };
|
|
1553
|
+
}
|
|
1554
|
+
if (status === 'pending' && new Date().toISOString() >= spec.expiresAt) {
|
|
1555
|
+
return { status: 'expired' };
|
|
1556
|
+
}
|
|
1557
|
+
return { status };
|
|
1558
|
+
}
|
|
1464
1559
|
async function pollLinkSession(spec) {
|
|
1465
1560
|
for (;;) {
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
if (status === 'completed' && body.secret) {
|
|
1470
|
-
return { status, secret: body.secret };
|
|
1471
|
-
}
|
|
1472
|
-
if (status === 'expired' || status === 'consumed' || status === 'invalid') {
|
|
1473
|
-
return { status };
|
|
1561
|
+
const result = await pollLinkSessionOnce(spec);
|
|
1562
|
+
if (result.status === 'completed' && result.secret) {
|
|
1563
|
+
return result;
|
|
1474
1564
|
}
|
|
1475
|
-
if (
|
|
1476
|
-
|
|
1565
|
+
if (result.status === 'expired' ||
|
|
1566
|
+
result.status === 'consumed' ||
|
|
1567
|
+
result.status === 'invalid') {
|
|
1568
|
+
return result;
|
|
1477
1569
|
}
|
|
1478
1570
|
await new Promise((resolveDelay) => setTimeout(resolveDelay, SCREENCI_LINK_SESSION_POLL_INTERVAL_MS));
|
|
1479
1571
|
}
|
|
1480
1572
|
}
|
|
1481
|
-
export async function ensureScreenciSecret(resolvedConfigPath) {
|
|
1573
|
+
export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
1574
|
+
const interactive = opts.interactive ?? true;
|
|
1482
1575
|
const existingSecret = process.env.SCREENCI_SECRET;
|
|
1483
1576
|
if (existingSecret)
|
|
1484
1577
|
return existingSecret;
|
|
@@ -1498,7 +1591,7 @@ export async function ensureScreenciSecret(resolvedConfigPath) {
|
|
|
1498
1591
|
envFilePath,
|
|
1499
1592
|
...(resolvedConfigPath ? { resolvedConfigPath } : {}),
|
|
1500
1593
|
};
|
|
1501
|
-
|
|
1594
|
+
const ensureSpec = async () => {
|
|
1502
1595
|
const storedSpec = await readPersistedLinkSessionSpec(specPath);
|
|
1503
1596
|
const spec = storedSpec &&
|
|
1504
1597
|
isStoredLinkSessionReusable(storedSpec, linkSessionContext)
|
|
@@ -1511,14 +1604,44 @@ export async function ensureScreenciSecret(resolvedConfigPath) {
|
|
|
1511
1604
|
if (spec !== storedSpec) {
|
|
1512
1605
|
await writePersistedLinkSessionSpec(specPath, spec);
|
|
1513
1606
|
}
|
|
1607
|
+
return spec;
|
|
1608
|
+
};
|
|
1609
|
+
const saveCompletedSecret = async (secret) => {
|
|
1610
|
+
process.env.SCREENCI_SECRET = secret;
|
|
1611
|
+
await persistScreenCISecret(envFilePath, secret);
|
|
1612
|
+
deletePersistedLinkSessionSpec(specPath);
|
|
1613
|
+
logger.info(`Successfully saved SCREENCI_SECRET to ${envFilePath}`);
|
|
1614
|
+
return secret;
|
|
1615
|
+
};
|
|
1616
|
+
if (!interactive) {
|
|
1617
|
+
// Non-interactive sessions cannot complete a browser sign-in here, so we
|
|
1618
|
+
// never block. Reuse or create the persisted session and check its status
|
|
1619
|
+
// once; if a stored session is stale, recreate it and check once more.
|
|
1620
|
+
// When the session is already completed (the sign-in happened between
|
|
1621
|
+
// runs) we pick up the secret; otherwise we print the link and return so
|
|
1622
|
+
// the caller can surface it and rerun later.
|
|
1623
|
+
let spec = await ensureSpec();
|
|
1624
|
+
let result = await pollLinkSessionOnce(spec);
|
|
1625
|
+
if (result.status === 'expired' ||
|
|
1626
|
+
result.status === 'consumed' ||
|
|
1627
|
+
result.status === 'invalid') {
|
|
1628
|
+
deletePersistedLinkSessionSpec(specPath);
|
|
1629
|
+
spec = await ensureSpec();
|
|
1630
|
+
result = await pollLinkSessionOnce(spec);
|
|
1631
|
+
}
|
|
1632
|
+
if (result.status === 'completed' && result.secret) {
|
|
1633
|
+
return await saveCompletedSecret(result.secret);
|
|
1634
|
+
}
|
|
1635
|
+
logger.info(`Sign-in required to record. Open this link to sign in and choose a plan:\n${pc.cyan(spec.appUrl)}\n` +
|
|
1636
|
+
`This session is non-interactive, so sign-in can't complete here. After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to continue.`);
|
|
1637
|
+
return undefined;
|
|
1638
|
+
}
|
|
1639
|
+
for (;;) {
|
|
1640
|
+
const spec = await ensureSpec();
|
|
1514
1641
|
logger.info(`Open this link to sign in and connect the CLI:\n${pc.cyan(spec.appUrl)}\n`);
|
|
1515
1642
|
const result = await pollLinkSession(spec);
|
|
1516
1643
|
if (result.status === 'completed' && result.secret) {
|
|
1517
|
-
|
|
1518
|
-
await persistScreenCISecret(envFilePath, result.secret);
|
|
1519
|
-
deletePersistedLinkSessionSpec(specPath);
|
|
1520
|
-
logger.info(`Successfully saved SCREENCI_SECRET to ${envFilePath}`);
|
|
1521
|
-
return result.secret;
|
|
1644
|
+
return await saveCompletedSecret(result.secret);
|
|
1522
1645
|
}
|
|
1523
1646
|
deletePersistedLinkSessionSpec(specPath);
|
|
1524
1647
|
}
|
|
@@ -1565,9 +1688,12 @@ export async function main() {
|
|
|
1565
1688
|
}
|
|
1566
1689
|
if (process.env.SCREENCI_RECORDING === 'true')
|
|
1567
1690
|
return;
|
|
1568
|
-
// After recording, upload results to API if configured
|
|
1569
|
-
|
|
1570
|
-
|
|
1691
|
+
// After recording, upload results to API if configured. `run` already
|
|
1692
|
+
// resolved the config (or exited), so this best-effort lookup only acts
|
|
1693
|
+
// when a flat config is present in/under the current directory.
|
|
1694
|
+
const resolution = findScreenCIConfig(parsed.configPath);
|
|
1695
|
+
if (resolution.kind === 'found') {
|
|
1696
|
+
const resolvedConfigPath = resolution.path;
|
|
1571
1697
|
try {
|
|
1572
1698
|
const screenciConfig = await loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath);
|
|
1573
1699
|
loadEnvFile(screenciConfig.envFile
|
|
@@ -1674,8 +1800,11 @@ export async function main() {
|
|
|
1674
1800
|
.action(async () => {
|
|
1675
1801
|
const parsed = parseConfigCliArgs(getSubcommandArgv('test'));
|
|
1676
1802
|
let configMockRecord = false;
|
|
1677
|
-
|
|
1678
|
-
|
|
1803
|
+
// Best-effort env preload before handing off to `run`, which performs the
|
|
1804
|
+
// authoritative resolution (and emits the `cd screenci` guidance on miss).
|
|
1805
|
+
const resolution = findScreenCIConfig(parsed.configPath);
|
|
1806
|
+
if (resolution.kind === 'found') {
|
|
1807
|
+
const resolvedConfigPath = resolution.path;
|
|
1679
1808
|
try {
|
|
1680
1809
|
const screenciConfig = await loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath);
|
|
1681
1810
|
configMockRecord = screenciConfig.test?.mockRecord ?? false;
|
|
@@ -1846,21 +1975,16 @@ function validateArgs(args) {
|
|
|
1846
1975
|
}
|
|
1847
1976
|
}
|
|
1848
1977
|
async function run(command, additionalArgs, customConfigPath, verbose = false, mockRecord = false) {
|
|
1849
|
-
const configPath =
|
|
1850
|
-
if (!configPath) {
|
|
1851
|
-
const errorMsg = customConfigPath
|
|
1852
|
-
? `Error: Config file not found: ${customConfigPath}`
|
|
1853
|
-
: 'Error: screenci.config.ts not found in current directory';
|
|
1854
|
-
logger.error(errorMsg);
|
|
1855
|
-
process.exit(1);
|
|
1856
|
-
}
|
|
1978
|
+
const configPath = resolveScreenCIConfigPathOrExit(customConfigPath);
|
|
1857
1979
|
if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
|
|
1858
1980
|
await loadEnvFileFromConfigSource(configPath, false);
|
|
1859
1981
|
}
|
|
1860
1982
|
// Only validate args for record command
|
|
1861
1983
|
if (command === 'record') {
|
|
1862
1984
|
if (!process.env.SCREENCI_SECRET) {
|
|
1863
|
-
await requireScreenCISecret(configPath
|
|
1985
|
+
await requireScreenCISecret(configPath, {
|
|
1986
|
+
interactive: detectInteractiveSession(),
|
|
1987
|
+
});
|
|
1864
1988
|
}
|
|
1865
1989
|
validateArgs(additionalArgs);
|
|
1866
1990
|
const screenciDir = resolve(dirname(configPath), '.screenci');
|