screenci 0.0.63 → 0.0.64
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 +172 -54
- 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 +292 -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) => {
|
|
@@ -1135,14 +1212,7 @@ async function writeGitHubProjectOutput(projectUrl) {
|
|
|
1135
1212
|
await appendFile(githubOutput, `screenci_project_url=${projectUrl}\n`);
|
|
1136
1213
|
}
|
|
1137
1214
|
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
|
-
}
|
|
1215
|
+
const resolvedConfigPath = resolveScreenCIConfigPathOrExit(configPath);
|
|
1146
1216
|
let screenciConfig;
|
|
1147
1217
|
try {
|
|
1148
1218
|
screenciConfig =
|
|
@@ -1334,11 +1404,16 @@ async function loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath) {
|
|
|
1334
1404
|
throw err;
|
|
1335
1405
|
}
|
|
1336
1406
|
}
|
|
1337
|
-
async function requireScreenCISecret(configPath) {
|
|
1407
|
+
async function requireScreenCISecret(configPath, opts = {}) {
|
|
1338
1408
|
const { resolvedConfigPath, screenciConfig } = await loadScreenCIConfigAndEnv(configPath);
|
|
1339
1409
|
const secret = process.env.SCREENCI_SECRET ??
|
|
1340
|
-
(await ensureScreenciSecret(resolvedConfigPath));
|
|
1410
|
+
(await ensureScreenciSecret(resolvedConfigPath, opts));
|
|
1341
1411
|
if (!secret) {
|
|
1412
|
+
// In a non-interactive session ensureScreenciSecret already printed the
|
|
1413
|
+
// sign-in link and the next step, so we exit without repeating guidance.
|
|
1414
|
+
if (opts.interactive === false) {
|
|
1415
|
+
process.exit(1);
|
|
1416
|
+
}
|
|
1342
1417
|
const envFilePath = await resolveProjectEnvFilePath(resolvedConfigPath);
|
|
1343
1418
|
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
1419
|
process.exit(1);
|
|
@@ -1351,7 +1426,7 @@ async function requireScreenCISecret(configPath) {
|
|
|
1351
1426
|
};
|
|
1352
1427
|
}
|
|
1353
1428
|
async function fetchProjectInfo(configPath) {
|
|
1354
|
-
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
1429
|
+
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath, { interactive: detectInteractiveSession() });
|
|
1355
1430
|
const url = new URL(`${apiUrl}/cli/project-info`);
|
|
1356
1431
|
url.searchParams.set('projectName', screenciConfig.projectName);
|
|
1357
1432
|
const res = await fetch(url.toString(), {
|
|
@@ -1370,7 +1445,9 @@ async function printProjectInfo(configPath) {
|
|
|
1370
1445
|
process.stdout.write(`${JSON.stringify(info, null, 2)}\n`);
|
|
1371
1446
|
}
|
|
1372
1447
|
async function updateVideoVisibility(videoId, isPublic, configPath) {
|
|
1373
|
-
const { secret, apiUrl } = await requireScreenCISecret(configPath
|
|
1448
|
+
const { secret, apiUrl } = await requireScreenCISecret(configPath, {
|
|
1449
|
+
interactive: detectInteractiveSession(),
|
|
1450
|
+
});
|
|
1374
1451
|
const method = isPublic ? 'PUT' : 'DELETE';
|
|
1375
1452
|
const res = await fetch(`${apiUrl}/cli/public-video/${videoId}`, {
|
|
1376
1453
|
method,
|
|
@@ -1461,24 +1538,34 @@ async function createLinkSessionSpec(options) {
|
|
|
1461
1538
|
envFilePath: options.envFilePath,
|
|
1462
1539
|
};
|
|
1463
1540
|
}
|
|
1541
|
+
async function pollLinkSessionOnce(spec) {
|
|
1542
|
+
const response = await fetch(spec.pollUrl);
|
|
1543
|
+
const body = (await response.json());
|
|
1544
|
+
const status = body.status ?? 'invalid';
|
|
1545
|
+
if (status === 'completed' && body.secret) {
|
|
1546
|
+
return { status, secret: body.secret };
|
|
1547
|
+
}
|
|
1548
|
+
if (status === 'pending' && new Date().toISOString() >= spec.expiresAt) {
|
|
1549
|
+
return { status: 'expired' };
|
|
1550
|
+
}
|
|
1551
|
+
return { status };
|
|
1552
|
+
}
|
|
1464
1553
|
async function pollLinkSession(spec) {
|
|
1465
1554
|
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 };
|
|
1555
|
+
const result = await pollLinkSessionOnce(spec);
|
|
1556
|
+
if (result.status === 'completed' && result.secret) {
|
|
1557
|
+
return result;
|
|
1474
1558
|
}
|
|
1475
|
-
if (
|
|
1476
|
-
|
|
1559
|
+
if (result.status === 'expired' ||
|
|
1560
|
+
result.status === 'consumed' ||
|
|
1561
|
+
result.status === 'invalid') {
|
|
1562
|
+
return result;
|
|
1477
1563
|
}
|
|
1478
1564
|
await new Promise((resolveDelay) => setTimeout(resolveDelay, SCREENCI_LINK_SESSION_POLL_INTERVAL_MS));
|
|
1479
1565
|
}
|
|
1480
1566
|
}
|
|
1481
|
-
export async function ensureScreenciSecret(resolvedConfigPath) {
|
|
1567
|
+
export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
1568
|
+
const interactive = opts.interactive ?? true;
|
|
1482
1569
|
const existingSecret = process.env.SCREENCI_SECRET;
|
|
1483
1570
|
if (existingSecret)
|
|
1484
1571
|
return existingSecret;
|
|
@@ -1498,7 +1585,7 @@ export async function ensureScreenciSecret(resolvedConfigPath) {
|
|
|
1498
1585
|
envFilePath,
|
|
1499
1586
|
...(resolvedConfigPath ? { resolvedConfigPath } : {}),
|
|
1500
1587
|
};
|
|
1501
|
-
|
|
1588
|
+
const ensureSpec = async () => {
|
|
1502
1589
|
const storedSpec = await readPersistedLinkSessionSpec(specPath);
|
|
1503
1590
|
const spec = storedSpec &&
|
|
1504
1591
|
isStoredLinkSessionReusable(storedSpec, linkSessionContext)
|
|
@@ -1511,14 +1598,44 @@ export async function ensureScreenciSecret(resolvedConfigPath) {
|
|
|
1511
1598
|
if (spec !== storedSpec) {
|
|
1512
1599
|
await writePersistedLinkSessionSpec(specPath, spec);
|
|
1513
1600
|
}
|
|
1601
|
+
return spec;
|
|
1602
|
+
};
|
|
1603
|
+
const saveCompletedSecret = async (secret) => {
|
|
1604
|
+
process.env.SCREENCI_SECRET = secret;
|
|
1605
|
+
await persistScreenCISecret(envFilePath, secret);
|
|
1606
|
+
deletePersistedLinkSessionSpec(specPath);
|
|
1607
|
+
logger.info(`Successfully saved SCREENCI_SECRET to ${envFilePath}`);
|
|
1608
|
+
return secret;
|
|
1609
|
+
};
|
|
1610
|
+
if (!interactive) {
|
|
1611
|
+
// Non-interactive sessions cannot complete a browser sign-in here, so we
|
|
1612
|
+
// never block. Reuse or create the persisted session and check its status
|
|
1613
|
+
// once; if a stored session is stale, recreate it and check once more.
|
|
1614
|
+
// When the session is already completed (the sign-in happened between
|
|
1615
|
+
// runs) we pick up the secret; otherwise we print the link and return so
|
|
1616
|
+
// the caller can surface it and rerun later.
|
|
1617
|
+
let spec = await ensureSpec();
|
|
1618
|
+
let result = await pollLinkSessionOnce(spec);
|
|
1619
|
+
if (result.status === 'expired' ||
|
|
1620
|
+
result.status === 'consumed' ||
|
|
1621
|
+
result.status === 'invalid') {
|
|
1622
|
+
deletePersistedLinkSessionSpec(specPath);
|
|
1623
|
+
spec = await ensureSpec();
|
|
1624
|
+
result = await pollLinkSessionOnce(spec);
|
|
1625
|
+
}
|
|
1626
|
+
if (result.status === 'completed' && result.secret) {
|
|
1627
|
+
return await saveCompletedSecret(result.secret);
|
|
1628
|
+
}
|
|
1629
|
+
logger.info(`Sign-in required to record. Open this link to sign in and choose a plan:\n${pc.cyan(spec.appUrl)}\n` +
|
|
1630
|
+
`This session is non-interactive, so sign-in can't complete here. After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to continue.`);
|
|
1631
|
+
return undefined;
|
|
1632
|
+
}
|
|
1633
|
+
for (;;) {
|
|
1634
|
+
const spec = await ensureSpec();
|
|
1514
1635
|
logger.info(`Open this link to sign in and connect the CLI:\n${pc.cyan(spec.appUrl)}\n`);
|
|
1515
1636
|
const result = await pollLinkSession(spec);
|
|
1516
1637
|
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;
|
|
1638
|
+
return await saveCompletedSecret(result.secret);
|
|
1522
1639
|
}
|
|
1523
1640
|
deletePersistedLinkSessionSpec(specPath);
|
|
1524
1641
|
}
|
|
@@ -1565,9 +1682,12 @@ export async function main() {
|
|
|
1565
1682
|
}
|
|
1566
1683
|
if (process.env.SCREENCI_RECORDING === 'true')
|
|
1567
1684
|
return;
|
|
1568
|
-
// After recording, upload results to API if configured
|
|
1569
|
-
|
|
1570
|
-
|
|
1685
|
+
// After recording, upload results to API if configured. `run` already
|
|
1686
|
+
// resolved the config (or exited), so this best-effort lookup only acts
|
|
1687
|
+
// when a flat config is present in/under the current directory.
|
|
1688
|
+
const resolution = findScreenCIConfig(parsed.configPath);
|
|
1689
|
+
if (resolution.kind === 'found') {
|
|
1690
|
+
const resolvedConfigPath = resolution.path;
|
|
1571
1691
|
try {
|
|
1572
1692
|
const screenciConfig = await loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath);
|
|
1573
1693
|
loadEnvFile(screenciConfig.envFile
|
|
@@ -1674,8 +1794,11 @@ export async function main() {
|
|
|
1674
1794
|
.action(async () => {
|
|
1675
1795
|
const parsed = parseConfigCliArgs(getSubcommandArgv('test'));
|
|
1676
1796
|
let configMockRecord = false;
|
|
1677
|
-
|
|
1678
|
-
|
|
1797
|
+
// Best-effort env preload before handing off to `run`, which performs the
|
|
1798
|
+
// authoritative resolution (and emits the `cd screenci` guidance on miss).
|
|
1799
|
+
const resolution = findScreenCIConfig(parsed.configPath);
|
|
1800
|
+
if (resolution.kind === 'found') {
|
|
1801
|
+
const resolvedConfigPath = resolution.path;
|
|
1679
1802
|
try {
|
|
1680
1803
|
const screenciConfig = await loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath);
|
|
1681
1804
|
configMockRecord = screenciConfig.test?.mockRecord ?? false;
|
|
@@ -1846,21 +1969,16 @@ function validateArgs(args) {
|
|
|
1846
1969
|
}
|
|
1847
1970
|
}
|
|
1848
1971
|
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
|
-
}
|
|
1972
|
+
const configPath = resolveScreenCIConfigPathOrExit(customConfigPath);
|
|
1857
1973
|
if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
|
|
1858
1974
|
await loadEnvFileFromConfigSource(configPath, false);
|
|
1859
1975
|
}
|
|
1860
1976
|
// Only validate args for record command
|
|
1861
1977
|
if (command === 'record') {
|
|
1862
1978
|
if (!process.env.SCREENCI_SECRET) {
|
|
1863
|
-
await requireScreenCISecret(configPath
|
|
1979
|
+
await requireScreenCISecret(configPath, {
|
|
1980
|
+
interactive: detectInteractiveSession(),
|
|
1981
|
+
});
|
|
1864
1982
|
}
|
|
1865
1983
|
validateArgs(additionalArgs);
|
|
1866
1984
|
const screenciDir = resolve(dirname(configPath), '.screenci');
|