screenci 0.0.84 → 0.0.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +29 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +113 -28
- package/dist/cli.js.map +1 -1
- package/dist/docs/video-sources/speaking-style.screenci.js +79 -14
- package/dist/docs/video-sources/speaking-style.screenci.js.map +1 -1
- package/dist/docs/video-sources/vertical-video.screenci.js +1 -1
- package/dist/src/anonSession.d.ts +60 -0
- package/dist/src/anonSession.d.ts.map +1 -0
- package/dist/src/anonSession.js +115 -0
- package/dist/src/anonSession.js.map +1 -0
- package/dist/src/asset.d.ts.map +1 -1
- package/dist/src/asset.js +2 -2
- package/dist/src/asset.js.map +1 -1
- package/dist/src/audio.d.ts.map +1 -1
- package/dist/src/audio.js +2 -2
- package/dist/src/audio.js.map +1 -1
- package/dist/src/cue.d.ts.map +1 -1
- package/dist/src/cue.js +12 -4
- package/dist/src/cue.js.map +1 -1
- package/dist/src/events.d.ts +5 -1
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js.map +1 -1
- package/dist/src/git.d.ts +2 -0
- package/dist/src/git.d.ts.map +1 -1
- package/dist/src/git.js +1 -1
- package/dist/src/git.js.map +1 -1
- package/dist/src/init.d.ts +6 -8
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +32 -70
- package/dist/src/init.js.map +1 -1
- package/dist/src/linkSession.d.ts +0 -21
- package/dist/src/linkSession.d.ts.map +1 -1
- package/dist/src/linkSession.js +4 -57
- package/dist/src/linkSession.js.map +1 -1
- package/dist/src/missingAssetLog.d.ts +8 -0
- package/dist/src/missingAssetLog.d.ts.map +1 -0
- package/dist/src/missingAssetLog.js +16 -0
- package/dist/src/missingAssetLog.js.map +1 -0
- package/dist/src/recordingData.d.ts +1 -1
- package/dist/src/recordingData.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/skills/screenci/SKILL.md +10 -9
- package/skills/screenci/references/init.md +5 -7
- package/skills/screenci/references/record.md +9 -7
package/dist/cli.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { RecordingCustomVoiceRef, RecordingData } from './src/events.js';
|
|
|
2
2
|
import type { VoiceKey } from './src/voices.js';
|
|
3
3
|
import type { RecordUploadPolicy, ScreenCIConfig } from './src/types.js';
|
|
4
4
|
import { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl } from './src/linkSession.js';
|
|
5
|
+
import { type CliCredential } from './src/anonSession.js';
|
|
5
6
|
export { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl };
|
|
7
|
+
export { secretCredential, anonCredential } from './src/anonSession.js';
|
|
6
8
|
type PlaywrightListReportSuite = {
|
|
7
9
|
title?: string;
|
|
8
10
|
specs?: Array<{
|
|
@@ -101,7 +103,7 @@ export type ResolveAssetsResponse = {
|
|
|
101
103
|
* Returns the assets that could not be resolved (no previous version), so the
|
|
102
104
|
* caller can fail the recording with actionable guidance.
|
|
103
105
|
*/
|
|
104
|
-
export declare function resolveMissingUploadAssets(assets: PreparedUploadAsset[], projectName: string, videoName: string, apiUrl: string,
|
|
106
|
+
export declare function resolveMissingUploadAssets(assets: PreparedUploadAsset[], projectName: string, videoName: string, apiUrl: string, credential: CliCredential, signal: AbortSignal, progressReporter?: {
|
|
105
107
|
info: (message: string) => void;
|
|
106
108
|
}): Promise<PreparedUploadAsset[]>;
|
|
107
109
|
/**
|
|
@@ -109,7 +111,7 @@ export declare function resolveMissingUploadAssets(assets: PreparedUploadAsset[]
|
|
|
109
111
|
* previously uploaded version to reuse.
|
|
110
112
|
*/
|
|
111
113
|
export declare function formatUnresolvedAssetMessage(videoName: string, unresolved: PreparedUploadAsset[]): string;
|
|
112
|
-
export declare function uploadRecordings(screenciDir: string, projectName: string, apiUrl: string,
|
|
114
|
+
export declare function uploadRecordings(screenciDir: string, projectName: string, apiUrl: string, credential: CliCredential, specificEntry?: string, verbose?: boolean): Promise<{
|
|
113
115
|
projectId: string | null;
|
|
114
116
|
recordId: string | null;
|
|
115
117
|
hadFailures: boolean;
|
|
@@ -135,6 +137,31 @@ export declare function requireScreenCISecret(configPath?: string): Promise<{
|
|
|
135
137
|
apiUrl: string;
|
|
136
138
|
}>;
|
|
137
139
|
export declare function extractGrep(args: string[]): string | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* Resolves the credential `record`'s upload should authenticate with. A real
|
|
142
|
+
* SCREENCI_SECRET wins outright. Otherwise, checks the locally stored anon
|
|
143
|
+
* trial token: `claimed` self-upgrades by writing the real secret into `.env`
|
|
144
|
+
* and deleting the local anon state (no manual step required); every other
|
|
145
|
+
* status proceeds with the anon token. Whether an anonymous recording is
|
|
146
|
+
* allowed to start at all is decided before recording (see
|
|
147
|
+
* `ensureAnonRecordingAllowedOrExit`), so this function does not re-gate or
|
|
148
|
+
* silently mint a new trial here; the server-side one-call cap remains the
|
|
149
|
+
* final backstop.
|
|
150
|
+
*/
|
|
151
|
+
export declare function resolveUploadCredential(screenciDir: string, apiUrl: string, envFilePath: string, secretFromEnv: string | undefined): Promise<{
|
|
152
|
+
credential: CliCredential;
|
|
153
|
+
usedAnonCredential: boolean;
|
|
154
|
+
}>;
|
|
155
|
+
/**
|
|
156
|
+
* Pre-recording gate for anonymous trials. Runs before Playwright so a spent
|
|
157
|
+
* or expired trial never wastes a full recording only to be refused at upload.
|
|
158
|
+
* With a real SCREENCI_SECRET present this is a no-op. Otherwise it checks the
|
|
159
|
+
* local anon token's server status and, when the one free trial is already
|
|
160
|
+
* used or the session has expired, prints a sign-up message and exits without
|
|
161
|
+
* recording. A first-run, pending-unused, or claimed session proceeds (the
|
|
162
|
+
* upload path handles the claimed self-upgrade).
|
|
163
|
+
*/
|
|
164
|
+
export declare function ensureAnonRecordingAllowedOrExit(screenciDir: string, apiUrl: string, appUrl: string, secretFromEnv: string | undefined): Promise<void>;
|
|
138
165
|
export declare function main(): Promise<void>;
|
|
139
166
|
export declare function parseRecordCliArgs(args: string[]): {
|
|
140
167
|
configPath: string | undefined;
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"AAiCA,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,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAKxE,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -14,12 +14,18 @@ import { determinePackageManager, initToggleOptionsFromCommander, parsePackageMa
|
|
|
14
14
|
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';
|
|
15
15
|
import { DEFAULT_RECORD_UPLOAD_POLICY } from './src/defaults.js';
|
|
16
16
|
import { findDuplicateTitles, formatDuplicateTitlesMessage, } from './src/titleValidation.js';
|
|
17
|
-
import { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl, getScreenCISecretsUrl, } from './src/linkSession.js';
|
|
17
|
+
import { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl, getScreenCISecretsUrl, persistScreenCISecret, } from './src/linkSession.js';
|
|
18
18
|
import { OVERLAY_CACHE_DIR_NAME } from './src/htmlRasterizer.js';
|
|
19
19
|
import { maybeExtractVoiceSampleAudio } from './src/voiceSampleAudio.js';
|
|
20
|
+
import { ANON_SESSION_FILE, anonCredential, checkAnonSessionStatus, deleteAnonSessionFile, evaluateAnonRecordingGate, getOrCreateAnonToken, secretCredential, } from './src/anonSession.js';
|
|
20
21
|
// Re-export the environment-aware URL helpers so existing importers (and tests)
|
|
21
22
|
// can keep importing them from the CLI entrypoint.
|
|
22
23
|
export { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl };
|
|
24
|
+
// Re-exported so test files that mock fs/fs/promises can obtain these via the
|
|
25
|
+
// same dynamic `await import('./cli')` they already use, rather than a static
|
|
26
|
+
// top-level import — a static import of `./src/anonSession.js` (which imports
|
|
27
|
+
// fs) would resolve before the test file's own mock variables initialize.
|
|
28
|
+
export { secretCredential, anonCredential } from './src/anonSession.js';
|
|
23
29
|
const SCREENCI_MOCK_RECORD_DOCS_URL = 'https://screenci.com/docs/reference/cli/#--mock-record';
|
|
24
30
|
const SCREENCI_RECORD_DOCS_URL = 'https://screenci.com/docs/reference/cli/#screenci-record';
|
|
25
31
|
// Records the recordId of the most recent `screenci record` upload so
|
|
@@ -348,7 +354,7 @@ async function loadUploadCandidate(screenciDir, entry, verbose) {
|
|
|
348
354
|
preparedUploadAssets,
|
|
349
355
|
};
|
|
350
356
|
}
|
|
351
|
-
async function uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl,
|
|
357
|
+
async function uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl, credential, verbose, uploadAbort, progressReporter, progressIndex, recordId, expectedScreenshotCount) {
|
|
352
358
|
const { entry, videoName, data: rawData, preparedUploadAssets } = candidate;
|
|
353
359
|
let projectId = null;
|
|
354
360
|
let videoId = null;
|
|
@@ -379,7 +385,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
379
385
|
// for overlays, no hash. Recover their identity from a previous upload of
|
|
380
386
|
// this video before starting, so the recording data references them by hash
|
|
381
387
|
// and the backend existence check confirms they are still stored.
|
|
382
|
-
const unresolved = await resolveMissingUploadAssets(preparedUploadAssets, projectName, videoName, apiUrl,
|
|
388
|
+
const unresolved = await resolveMissingUploadAssets(preparedUploadAssets, projectName, videoName, apiUrl, credential, uploadAbort.signal, progressReporter);
|
|
383
389
|
if (unresolved.length > 0) {
|
|
384
390
|
progressReporter.complete(progressIndex, 'failure');
|
|
385
391
|
return {
|
|
@@ -397,7 +403,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
397
403
|
method: 'POST',
|
|
398
404
|
headers: {
|
|
399
405
|
'Content-Type': 'application/json',
|
|
400
|
-
|
|
406
|
+
[credential.header]: credential.value,
|
|
401
407
|
},
|
|
402
408
|
body: JSON.stringify({
|
|
403
409
|
projectName,
|
|
@@ -440,7 +446,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
440
446
|
videoId: null,
|
|
441
447
|
hadFailure: true,
|
|
442
448
|
videoName,
|
|
443
|
-
failureMessage: formatUploadStartFailureMessage(videoName, startResponse.status, text,
|
|
449
|
+
failureMessage: formatUploadStartFailureMessage(videoName, startResponse.status, text, credential.value),
|
|
444
450
|
recordId,
|
|
445
451
|
};
|
|
446
452
|
}
|
|
@@ -465,7 +471,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
465
471
|
logger.info(`recordingId=${recordingId} projectId=${projectId}`);
|
|
466
472
|
logger.info(`assets=${preparedUploadAssets.length} recordingHash=${recordingHash ?? 'none'}`);
|
|
467
473
|
}
|
|
468
|
-
await uploadAssets(preparedUploadAssets, apiUrl,
|
|
474
|
+
await uploadAssets(preparedUploadAssets, apiUrl, credential, recordingId, uploadAbort.signal, uploadAbort.throwIfAborted, progressReporter);
|
|
469
475
|
uploadAbort.throwIfAborted();
|
|
470
476
|
const fileStat = await stat(recordingPath);
|
|
471
477
|
if (verbose) {
|
|
@@ -483,7 +489,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
483
489
|
headers: {
|
|
484
490
|
'Content-Type': recordingContentType,
|
|
485
491
|
'Content-Length': String(fileStat.size),
|
|
486
|
-
|
|
492
|
+
[credential.header]: credential.value,
|
|
487
493
|
},
|
|
488
494
|
body: stream,
|
|
489
495
|
signal: uploadAbort.signal,
|
|
@@ -503,7 +509,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
503
509
|
videoId,
|
|
504
510
|
hadFailure: true,
|
|
505
511
|
videoName,
|
|
506
|
-
failureMessage: `Failed to upload recording for "${videoName}": ${recordingResponse.status} ${extractBackendError(text)}${hint401(recordingResponse.status,
|
|
512
|
+
failureMessage: `Failed to upload recording for "${videoName}": ${recordingResponse.status} ${extractBackendError(text)}${hint401(recordingResponse.status, credential.value)}`,
|
|
507
513
|
recordId,
|
|
508
514
|
...(plan !== null && { plan }),
|
|
509
515
|
};
|
|
@@ -754,6 +760,11 @@ export function clearRecordingDirectories(dir) {
|
|
|
754
760
|
// each run, changing their content hashes and forcing a re-upload.
|
|
755
761
|
if (entry === OVERLAY_CACHE_DIR_NAME)
|
|
756
762
|
continue;
|
|
763
|
+
// Preserve the anon trial token: it identifies one continuous trial across
|
|
764
|
+
// runs. Wiping it would mint a fresh trial every record, so the one-record
|
|
765
|
+
// cap, the claim, and the auto-graduate to a real secret would all break.
|
|
766
|
+
if (entry === ANON_SESSION_FILE)
|
|
767
|
+
continue;
|
|
757
768
|
rmSync(resolve(dir, entry), { recursive: true, force: true });
|
|
758
769
|
}
|
|
759
770
|
}
|
|
@@ -1347,7 +1358,7 @@ export function displayAssetPath(assetPath) {
|
|
|
1347
1358
|
* Returns the assets that could not be resolved (no previous version), so the
|
|
1348
1359
|
* caller can fail the recording with actionable guidance.
|
|
1349
1360
|
*/
|
|
1350
|
-
export async function resolveMissingUploadAssets(assets, projectName, videoName, apiUrl,
|
|
1361
|
+
export async function resolveMissingUploadAssets(assets, projectName, videoName, apiUrl, credential, signal, progressReporter) {
|
|
1351
1362
|
const pending = assets.filter((asset) => asset.needsResolve === true);
|
|
1352
1363
|
if (pending.length === 0)
|
|
1353
1364
|
return [];
|
|
@@ -1360,14 +1371,14 @@ export async function resolveMissingUploadAssets(assets, projectName, videoName,
|
|
|
1360
1371
|
method: 'POST',
|
|
1361
1372
|
headers: {
|
|
1362
1373
|
'Content-Type': 'application/json',
|
|
1363
|
-
|
|
1374
|
+
[credential.header]: credential.value,
|
|
1364
1375
|
},
|
|
1365
1376
|
body: JSON.stringify({ projectName, videoName, assets: refs }),
|
|
1366
1377
|
signal,
|
|
1367
1378
|
}), signal);
|
|
1368
1379
|
if (!res.ok) {
|
|
1369
1380
|
const text = await res.text();
|
|
1370
|
-
throw new UploadAssetError(`Failed to resolve previously uploaded assets for "${videoName}": ${res.status} ${extractBackendError(text)}${hint401(res.status,
|
|
1381
|
+
throw new UploadAssetError(`Failed to resolve previously uploaded assets for "${videoName}": ${res.status} ${extractBackendError(text)}${hint401(res.status, credential.value)}`);
|
|
1371
1382
|
}
|
|
1372
1383
|
const body = (await res.json());
|
|
1373
1384
|
const resolved = Array.isArray(body.resolved) ? body.resolved : [];
|
|
@@ -1410,7 +1421,7 @@ export function formatUnresolvedAssetMessage(videoName, unresolved) {
|
|
|
1410
1421
|
'Record once with these files present so they are uploaded, or commit them so they are available here.',
|
|
1411
1422
|
].join('\n');
|
|
1412
1423
|
}
|
|
1413
|
-
async function uploadAssets(assets, apiUrl,
|
|
1424
|
+
async function uploadAssets(assets, apiUrl, credential, recordingId, signal, throwIfAborted, progressReporter) {
|
|
1414
1425
|
const logInfo = (message) => {
|
|
1415
1426
|
if (progressReporter) {
|
|
1416
1427
|
progressReporter.info(message);
|
|
@@ -1428,7 +1439,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1428
1439
|
method: 'POST',
|
|
1429
1440
|
headers: {
|
|
1430
1441
|
'Content-Type': 'application/json',
|
|
1431
|
-
|
|
1442
|
+
[credential.header]: credential.value,
|
|
1432
1443
|
},
|
|
1433
1444
|
body: JSON.stringify({
|
|
1434
1445
|
fileHash: asset.fileHash,
|
|
@@ -1441,7 +1452,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1441
1452
|
}), signal);
|
|
1442
1453
|
if (!checkRes.ok) {
|
|
1443
1454
|
const text = await checkRes.text();
|
|
1444
|
-
throw new UploadAssetError(`Failed to check asset ${displayAssetPath(asset.path)}: ${checkRes.status} ${extractBackendError(text)}${hint401(checkRes.status,
|
|
1455
|
+
throw new UploadAssetError(`Failed to check asset ${displayAssetPath(asset.path)}: ${checkRes.status} ${extractBackendError(text)}${hint401(checkRes.status, credential.value)}`);
|
|
1445
1456
|
}
|
|
1446
1457
|
const checkBody = (await checkRes.json());
|
|
1447
1458
|
if (checkBody.exists) {
|
|
@@ -1471,7 +1482,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1471
1482
|
headers: {
|
|
1472
1483
|
'Content-Type': contentType,
|
|
1473
1484
|
'Content-Length': String(fileBuffer.byteLength),
|
|
1474
|
-
|
|
1485
|
+
[credential.header]: credential.value,
|
|
1475
1486
|
'X-ScreenCI-File-Hash': asset.fileHash,
|
|
1476
1487
|
'X-ScreenCI-Asset-Size': String(asset.size),
|
|
1477
1488
|
'X-ScreenCI-Asset-Path': encodeURIComponent(asset.path),
|
|
@@ -1485,7 +1496,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1485
1496
|
logInfo(`${pc.green('✔')} ${label} already exists: ${displayAssetPath(asset.path)}`);
|
|
1486
1497
|
}
|
|
1487
1498
|
else {
|
|
1488
|
-
throw new UploadAssetError(`Failed to upload asset ${displayAssetPath(asset.path)}: ${res.status} ${extractBackendError(text)}${hint401(res.status,
|
|
1499
|
+
throw new UploadAssetError(`Failed to upload asset ${displayAssetPath(asset.path)}: ${res.status} ${extractBackendError(text)}${hint401(res.status, credential.value)}`);
|
|
1489
1500
|
}
|
|
1490
1501
|
}
|
|
1491
1502
|
else {
|
|
@@ -1503,7 +1514,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1503
1514
|
}
|
|
1504
1515
|
}
|
|
1505
1516
|
}
|
|
1506
|
-
export async function uploadRecordings(screenciDir, projectName, apiUrl,
|
|
1517
|
+
export async function uploadRecordings(screenciDir, projectName, apiUrl, credential, specificEntry, verbose = false) {
|
|
1507
1518
|
const uploadAbort = createUploadAbortController('upload');
|
|
1508
1519
|
const recordId = randomUUID();
|
|
1509
1520
|
let entries;
|
|
@@ -1551,7 +1562,7 @@ export async function uploadRecordings(screenciDir, projectName, apiUrl, secret,
|
|
|
1551
1562
|
// waits for all of them to land before dispatching the batch, so it needs
|
|
1552
1563
|
// to know how many to expect.
|
|
1553
1564
|
const screenshotCount = candidates.filter((candidate) => candidate.data.output === 'screenshot').length;
|
|
1554
|
-
const results = await Promise.all(candidates.map(async (candidate, index) => await uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl,
|
|
1565
|
+
const results = await Promise.all(candidates.map(async (candidate, index) => await uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl, credential, verbose, uploadAbort, progressReporter, index, recordId, screenshotCount)));
|
|
1555
1566
|
firstProjectId =
|
|
1556
1567
|
results.find((result) => result.projectId !== null)?.projectId ?? null;
|
|
1557
1568
|
const resolvedPlan = results.find((result) => result.plan !== undefined)?.plan ?? null;
|
|
@@ -1812,10 +1823,10 @@ export async function requireScreenCISecret(configPath) {
|
|
|
1812
1823
|
const { resolvedConfigPath, screenciConfig } = await loadScreenCIConfigAndEnv(configPath);
|
|
1813
1824
|
const secret = process.env.SCREENCI_SECRET;
|
|
1814
1825
|
if (!secret) {
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1826
|
+
// These commands need a real account (unlike `record`, which can upload
|
|
1827
|
+
// anonymously): guide the user to copy their secret and exit non-zero.
|
|
1817
1828
|
const envFilePath = await resolveProjectEnvFilePath(resolvedConfigPath);
|
|
1818
|
-
logger.error(`No SCREENCI_SECRET configured. Copy your secret from ${pc.cyan(getScreenCISecretsUrl())} into ${envFilePath}
|
|
1829
|
+
logger.error(`No SCREENCI_SECRET configured. Copy your secret from ${pc.cyan(getScreenCISecretsUrl())} into ${envFilePath}.`);
|
|
1819
1830
|
logScreenCISecretGuide();
|
|
1820
1831
|
process.exit(1);
|
|
1821
1832
|
}
|
|
@@ -1981,6 +1992,62 @@ async function printInfo(configPath) {
|
|
|
1981
1992
|
const data = await res.json();
|
|
1982
1993
|
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
1983
1994
|
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Resolves the credential `record`'s upload should authenticate with. A real
|
|
1997
|
+
* SCREENCI_SECRET wins outright. Otherwise, checks the locally stored anon
|
|
1998
|
+
* trial token: `claimed` self-upgrades by writing the real secret into `.env`
|
|
1999
|
+
* and deleting the local anon state (no manual step required); every other
|
|
2000
|
+
* status proceeds with the anon token. Whether an anonymous recording is
|
|
2001
|
+
* allowed to start at all is decided before recording (see
|
|
2002
|
+
* `ensureAnonRecordingAllowedOrExit`), so this function does not re-gate or
|
|
2003
|
+
* silently mint a new trial here; the server-side one-call cap remains the
|
|
2004
|
+
* final backstop.
|
|
2005
|
+
*/
|
|
2006
|
+
export async function resolveUploadCredential(screenciDir, apiUrl, envFilePath, secretFromEnv) {
|
|
2007
|
+
if (secretFromEnv) {
|
|
2008
|
+
return {
|
|
2009
|
+
credential: secretCredential(secretFromEnv),
|
|
2010
|
+
usedAnonCredential: false,
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
const token = await getOrCreateAnonToken(screenciDir);
|
|
2014
|
+
const status = await checkAnonSessionStatus(token, { backendUrl: apiUrl });
|
|
2015
|
+
if (status.status === 'claimed') {
|
|
2016
|
+
await persistScreenCISecret(envFilePath, status.secret);
|
|
2017
|
+
await deleteAnonSessionFile(screenciDir);
|
|
2018
|
+
logger.info('Linked to your ScreenCI account. Future uploads use your API key.');
|
|
2019
|
+
return {
|
|
2020
|
+
credential: secretCredential(status.secret),
|
|
2021
|
+
usedAnonCredential: false,
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
return { credential: anonCredential(token), usedAnonCredential: true };
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Pre-recording gate for anonymous trials. Runs before Playwright so a spent
|
|
2028
|
+
* or expired trial never wastes a full recording only to be refused at upload.
|
|
2029
|
+
* With a real SCREENCI_SECRET present this is a no-op. Otherwise it checks the
|
|
2030
|
+
* local anon token's server status and, when the one free trial is already
|
|
2031
|
+
* used or the session has expired, prints a sign-up message and exits without
|
|
2032
|
+
* recording. A first-run, pending-unused, or claimed session proceeds (the
|
|
2033
|
+
* upload path handles the claimed self-upgrade).
|
|
2034
|
+
*/
|
|
2035
|
+
export async function ensureAnonRecordingAllowedOrExit(screenciDir, apiUrl, appUrl, secretFromEnv) {
|
|
2036
|
+
if (secretFromEnv)
|
|
2037
|
+
return;
|
|
2038
|
+
const token = await getOrCreateAnonToken(screenciDir);
|
|
2039
|
+
const status = await checkAnonSessionStatus(token, { backendUrl: apiUrl });
|
|
2040
|
+
const gate = evaluateAnonRecordingGate(status);
|
|
2041
|
+
if (gate.allowed)
|
|
2042
|
+
return;
|
|
2043
|
+
const intro = gate.reason === 'expired'
|
|
2044
|
+
? 'Your free ScreenCI trial has expired.'
|
|
2045
|
+
: "You've already used your one free ScreenCI trial recording.";
|
|
2046
|
+
logger.error(`${intro}\n` +
|
|
2047
|
+
`Sign up to keep recording (no watermark, no limits): ${pc.cyan(appUrl)}\n` +
|
|
2048
|
+
'After signing up, re-run this command in the same folder and it links automatically.');
|
|
2049
|
+
process.exit(1);
|
|
2050
|
+
}
|
|
1984
2051
|
// Uploads the recordings already written under `.screenci` for the resolved
|
|
1985
2052
|
// config. Shared by `record` (after a Playwright run) and `retry` (which
|
|
1986
2053
|
// re-sends the existing recordings without re-running Playwright). A non-null
|
|
@@ -2001,17 +2068,17 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2001
2068
|
: resolve(dirname(resolvedConfigPath), '.env'), true);
|
|
2002
2069
|
const apiUrl = getDevBackendUrl();
|
|
2003
2070
|
const appUrl = getDevFrontendUrl();
|
|
2004
|
-
const secret = process.env.SCREENCI_SECRET;
|
|
2005
2071
|
const uploadPolicy = resolveRecordUploadPolicy(screenciConfig);
|
|
2006
2072
|
const configDir = dirname(resolvedConfigPath);
|
|
2007
2073
|
const screenciDir = resolve(configDir, '.screenci');
|
|
2074
|
+
const envFilePath = screenciConfig.envFile
|
|
2075
|
+
? resolve(configDir, screenciConfig.envFile)
|
|
2076
|
+
: resolve(configDir, '.env');
|
|
2008
2077
|
const completedRecordingCount = await countCompletedRecordings(screenciDir);
|
|
2078
|
+
const { credential, usedAnonCredential } = await resolveUploadCredential(screenciDir, apiUrl, envFilePath, process.env.SCREENCI_SECRET);
|
|
2009
2079
|
if (playwrightFailure !== null && completedRecordingCount === 0) {
|
|
2010
2080
|
logger.info('All recordings failed.');
|
|
2011
2081
|
}
|
|
2012
|
-
else if (!secret) {
|
|
2013
|
-
logger.info(`No SCREENCI_SECRET configured for uploads. Copy your secret from ${getScreenCISecretsUrl()} into the project env file, then rerun ${getSuggestedScreenciCommand('record')}.`);
|
|
2014
|
-
}
|
|
2015
2082
|
else if (playwrightFailure !== null &&
|
|
2016
2083
|
uploadPolicy === 'all-or-nothing') {
|
|
2017
2084
|
logger.info('Some recordings failed, skipping upload because record.upload is "all-or-nothing".');
|
|
@@ -2032,7 +2099,7 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2032
2099
|
plan: null,
|
|
2033
2100
|
};
|
|
2034
2101
|
try {
|
|
2035
|
-
uploadResult = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl,
|
|
2102
|
+
uploadResult = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, credential, undefined, verbose);
|
|
2036
2103
|
}
|
|
2037
2104
|
catch (err) {
|
|
2038
2105
|
if (isUploadCancelledError(err)) {
|
|
@@ -2075,6 +2142,9 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2075
2142
|
: 'Recording finished, rendering in progress. Results available at:');
|
|
2076
2143
|
logger.info(pc.cyan(projectUrl));
|
|
2077
2144
|
}
|
|
2145
|
+
if (usedAnonCredential && (recordId !== null || projectId !== null)) {
|
|
2146
|
+
logger.info('Recorded without an account. Open the link above to view it, and sign up to keep it (by continuing you agree to the Terms).');
|
|
2147
|
+
}
|
|
2078
2148
|
if (notices.length > 0) {
|
|
2079
2149
|
logger.info('');
|
|
2080
2150
|
for (const notice of notices) {
|
|
@@ -2406,6 +2476,11 @@ function validateArgs(args) {
|
|
|
2406
2476
|
* code seeds, and a recording is never blocked by this fetch.
|
|
2407
2477
|
*/
|
|
2408
2478
|
async function fetchTextOverridesEnv(configPath, languages, verbose) {
|
|
2479
|
+
// Studio config only exists for a real (non-anonymous) org, and record must
|
|
2480
|
+
// still work without an account, so skip the fetch entirely rather than
|
|
2481
|
+
// calling requireScreenCISecret, which would hard-exit without a secret.
|
|
2482
|
+
if (!process.env.SCREENCI_SECRET)
|
|
2483
|
+
return {};
|
|
2409
2484
|
try {
|
|
2410
2485
|
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
2411
2486
|
const params = new URLSearchParams({
|
|
@@ -2446,6 +2521,11 @@ async function fetchTextOverridesEnv(configPath, languages, verbose) {
|
|
|
2446
2521
|
* is never blocked by this fetch.
|
|
2447
2522
|
*/
|
|
2448
2523
|
async function fetchRecordOptionsEnv(configPath, verbose) {
|
|
2524
|
+
// Studio config only exists for a real (non-anonymous) org, and record must
|
|
2525
|
+
// still work without an account, so skip the fetch entirely rather than
|
|
2526
|
+
// calling requireScreenCISecret, which would hard-exit without a secret.
|
|
2527
|
+
if (!process.env.SCREENCI_SECRET)
|
|
2528
|
+
return {};
|
|
2449
2529
|
try {
|
|
2450
2530
|
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
2451
2531
|
const params = new URLSearchParams({
|
|
@@ -2480,12 +2560,17 @@ async function run(command, additionalArgs, customConfigPath, verbose = false, m
|
|
|
2480
2560
|
if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
|
|
2481
2561
|
await loadEnvFileFromConfigSource(configPath, false);
|
|
2482
2562
|
}
|
|
2483
|
-
// Only validate args for record command
|
|
2563
|
+
// Only validate args for record command. No secret is required here: record
|
|
2564
|
+
// can upload anonymously (see resolveUploadCredential), so this must not
|
|
2565
|
+
// hard-exit the way requireScreenCISecret does for account-only commands.
|
|
2484
2566
|
if (command === 'record') {
|
|
2485
|
-
await requireScreenCISecret(configPath);
|
|
2486
2567
|
validateArgs(additionalArgs);
|
|
2487
2568
|
const screenciDir = resolve(dirname(configPath), '.screenci');
|
|
2488
2569
|
clearRecordingDirectories(screenciDir);
|
|
2570
|
+
// Refuse a second anonymous trial recording before Playwright runs, so a
|
|
2571
|
+
// spent or expired trial never wastes a full render only to be rejected at
|
|
2572
|
+
// upload. No-op when a real SCREENCI_SECRET is set (env is loaded above).
|
|
2573
|
+
await ensureAnonRecordingAllowedOrExit(screenciDir, getDevBackendUrl(), getDevFrontendUrl(), process.env.SCREENCI_SECRET);
|
|
2489
2574
|
}
|
|
2490
2575
|
// Studio text-field overrides are injected for the record command only: they
|
|
2491
2576
|
// resolve in the SDK's `text` fixture before the recording runs. `test` (the
|