screenci 0.0.84 → 0.0.85
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 +15 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +81 -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 +40 -0
- package/dist/src/anonSession.d.ts.map +1 -0
- package/dist/src/anonSession.js +91 -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 +24 -45
- 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,17 @@ 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); `not_found`/
|
|
145
|
+
* `expired` nags and starts a fresh trial; `pending` proceeds anonymously.
|
|
146
|
+
*/
|
|
147
|
+
export declare function resolveUploadCredential(screenciDir: string, apiUrl: string, envFilePath: string, secretFromEnv: string | undefined): Promise<{
|
|
148
|
+
credential: CliCredential;
|
|
149
|
+
usedAnonCredential: boolean;
|
|
150
|
+
}>;
|
|
138
151
|
export declare function main(): Promise<void>;
|
|
139
152
|
export declare function parseRecordCliArgs(args: string[]): {
|
|
140
153
|
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 { anonCredential, checkAnonSessionStatus, deleteAnonSessionFile, 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
|
};
|
|
@@ -1347,7 +1353,7 @@ export function displayAssetPath(assetPath) {
|
|
|
1347
1353
|
* Returns the assets that could not be resolved (no previous version), so the
|
|
1348
1354
|
* caller can fail the recording with actionable guidance.
|
|
1349
1355
|
*/
|
|
1350
|
-
export async function resolveMissingUploadAssets(assets, projectName, videoName, apiUrl,
|
|
1356
|
+
export async function resolveMissingUploadAssets(assets, projectName, videoName, apiUrl, credential, signal, progressReporter) {
|
|
1351
1357
|
const pending = assets.filter((asset) => asset.needsResolve === true);
|
|
1352
1358
|
if (pending.length === 0)
|
|
1353
1359
|
return [];
|
|
@@ -1360,14 +1366,14 @@ export async function resolveMissingUploadAssets(assets, projectName, videoName,
|
|
|
1360
1366
|
method: 'POST',
|
|
1361
1367
|
headers: {
|
|
1362
1368
|
'Content-Type': 'application/json',
|
|
1363
|
-
|
|
1369
|
+
[credential.header]: credential.value,
|
|
1364
1370
|
},
|
|
1365
1371
|
body: JSON.stringify({ projectName, videoName, assets: refs }),
|
|
1366
1372
|
signal,
|
|
1367
1373
|
}), signal);
|
|
1368
1374
|
if (!res.ok) {
|
|
1369
1375
|
const text = await res.text();
|
|
1370
|
-
throw new UploadAssetError(`Failed to resolve previously uploaded assets for "${videoName}": ${res.status} ${extractBackendError(text)}${hint401(res.status,
|
|
1376
|
+
throw new UploadAssetError(`Failed to resolve previously uploaded assets for "${videoName}": ${res.status} ${extractBackendError(text)}${hint401(res.status, credential.value)}`);
|
|
1371
1377
|
}
|
|
1372
1378
|
const body = (await res.json());
|
|
1373
1379
|
const resolved = Array.isArray(body.resolved) ? body.resolved : [];
|
|
@@ -1410,7 +1416,7 @@ export function formatUnresolvedAssetMessage(videoName, unresolved) {
|
|
|
1410
1416
|
'Record once with these files present so they are uploaded, or commit them so they are available here.',
|
|
1411
1417
|
].join('\n');
|
|
1412
1418
|
}
|
|
1413
|
-
async function uploadAssets(assets, apiUrl,
|
|
1419
|
+
async function uploadAssets(assets, apiUrl, credential, recordingId, signal, throwIfAborted, progressReporter) {
|
|
1414
1420
|
const logInfo = (message) => {
|
|
1415
1421
|
if (progressReporter) {
|
|
1416
1422
|
progressReporter.info(message);
|
|
@@ -1428,7 +1434,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1428
1434
|
method: 'POST',
|
|
1429
1435
|
headers: {
|
|
1430
1436
|
'Content-Type': 'application/json',
|
|
1431
|
-
|
|
1437
|
+
[credential.header]: credential.value,
|
|
1432
1438
|
},
|
|
1433
1439
|
body: JSON.stringify({
|
|
1434
1440
|
fileHash: asset.fileHash,
|
|
@@ -1441,7 +1447,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1441
1447
|
}), signal);
|
|
1442
1448
|
if (!checkRes.ok) {
|
|
1443
1449
|
const text = await checkRes.text();
|
|
1444
|
-
throw new UploadAssetError(`Failed to check asset ${displayAssetPath(asset.path)}: ${checkRes.status} ${extractBackendError(text)}${hint401(checkRes.status,
|
|
1450
|
+
throw new UploadAssetError(`Failed to check asset ${displayAssetPath(asset.path)}: ${checkRes.status} ${extractBackendError(text)}${hint401(checkRes.status, credential.value)}`);
|
|
1445
1451
|
}
|
|
1446
1452
|
const checkBody = (await checkRes.json());
|
|
1447
1453
|
if (checkBody.exists) {
|
|
@@ -1471,7 +1477,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1471
1477
|
headers: {
|
|
1472
1478
|
'Content-Type': contentType,
|
|
1473
1479
|
'Content-Length': String(fileBuffer.byteLength),
|
|
1474
|
-
|
|
1480
|
+
[credential.header]: credential.value,
|
|
1475
1481
|
'X-ScreenCI-File-Hash': asset.fileHash,
|
|
1476
1482
|
'X-ScreenCI-Asset-Size': String(asset.size),
|
|
1477
1483
|
'X-ScreenCI-Asset-Path': encodeURIComponent(asset.path),
|
|
@@ -1485,7 +1491,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1485
1491
|
logInfo(`${pc.green('✔')} ${label} already exists: ${displayAssetPath(asset.path)}`);
|
|
1486
1492
|
}
|
|
1487
1493
|
else {
|
|
1488
|
-
throw new UploadAssetError(`Failed to upload asset ${displayAssetPath(asset.path)}: ${res.status} ${extractBackendError(text)}${hint401(res.status,
|
|
1494
|
+
throw new UploadAssetError(`Failed to upload asset ${displayAssetPath(asset.path)}: ${res.status} ${extractBackendError(text)}${hint401(res.status, credential.value)}`);
|
|
1489
1495
|
}
|
|
1490
1496
|
}
|
|
1491
1497
|
else {
|
|
@@ -1503,7 +1509,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1503
1509
|
}
|
|
1504
1510
|
}
|
|
1505
1511
|
}
|
|
1506
|
-
export async function uploadRecordings(screenciDir, projectName, apiUrl,
|
|
1512
|
+
export async function uploadRecordings(screenciDir, projectName, apiUrl, credential, specificEntry, verbose = false) {
|
|
1507
1513
|
const uploadAbort = createUploadAbortController('upload');
|
|
1508
1514
|
const recordId = randomUUID();
|
|
1509
1515
|
let entries;
|
|
@@ -1551,7 +1557,7 @@ export async function uploadRecordings(screenciDir, projectName, apiUrl, secret,
|
|
|
1551
1557
|
// waits for all of them to land before dispatching the batch, so it needs
|
|
1552
1558
|
// to know how many to expect.
|
|
1553
1559
|
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,
|
|
1560
|
+
const results = await Promise.all(candidates.map(async (candidate, index) => await uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl, credential, verbose, uploadAbort, progressReporter, index, recordId, screenshotCount)));
|
|
1555
1561
|
firstProjectId =
|
|
1556
1562
|
results.find((result) => result.projectId !== null)?.projectId ?? null;
|
|
1557
1563
|
const resolvedPlan = results.find((result) => result.plan !== undefined)?.plan ?? null;
|
|
@@ -1812,10 +1818,10 @@ export async function requireScreenCISecret(configPath) {
|
|
|
1812
1818
|
const { resolvedConfigPath, screenciConfig } = await loadScreenCIConfigAndEnv(configPath);
|
|
1813
1819
|
const secret = process.env.SCREENCI_SECRET;
|
|
1814
1820
|
if (!secret) {
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1821
|
+
// These commands need a real account (unlike `record`, which can upload
|
|
1822
|
+
// anonymously): guide the user to copy their secret and exit non-zero.
|
|
1817
1823
|
const envFilePath = await resolveProjectEnvFilePath(resolvedConfigPath);
|
|
1818
|
-
logger.error(`No SCREENCI_SECRET configured. Copy your secret from ${pc.cyan(getScreenCISecretsUrl())} into ${envFilePath}
|
|
1824
|
+
logger.error(`No SCREENCI_SECRET configured. Copy your secret from ${pc.cyan(getScreenCISecretsUrl())} into ${envFilePath}.`);
|
|
1819
1825
|
logScreenCISecretGuide();
|
|
1820
1826
|
process.exit(1);
|
|
1821
1827
|
}
|
|
@@ -1981,6 +1987,39 @@ async function printInfo(configPath) {
|
|
|
1981
1987
|
const data = await res.json();
|
|
1982
1988
|
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
1983
1989
|
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Resolves the credential `record`'s upload should authenticate with. A real
|
|
1992
|
+
* SCREENCI_SECRET wins outright. Otherwise, checks the locally stored anon
|
|
1993
|
+
* trial token: `claimed` self-upgrades by writing the real secret into `.env`
|
|
1994
|
+
* and deleting the local anon state (no manual step required); `not_found`/
|
|
1995
|
+
* `expired` nags and starts a fresh trial; `pending` proceeds anonymously.
|
|
1996
|
+
*/
|
|
1997
|
+
export async function resolveUploadCredential(screenciDir, apiUrl, envFilePath, secretFromEnv) {
|
|
1998
|
+
if (secretFromEnv) {
|
|
1999
|
+
return {
|
|
2000
|
+
credential: secretCredential(secretFromEnv),
|
|
2001
|
+
usedAnonCredential: false,
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
const token = await getOrCreateAnonToken(screenciDir);
|
|
2005
|
+
const status = await checkAnonSessionStatus(token, { backendUrl: apiUrl });
|
|
2006
|
+
if (status.status === 'claimed') {
|
|
2007
|
+
await persistScreenCISecret(envFilePath, status.secret);
|
|
2008
|
+
await deleteAnonSessionFile(screenciDir);
|
|
2009
|
+
logger.info('Linked to your ScreenCI account. Future uploads use your API key.');
|
|
2010
|
+
return {
|
|
2011
|
+
credential: secretCredential(status.secret),
|
|
2012
|
+
usedAnonCredential: false,
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
if (status.status === 'not_found' || status.status === 'expired') {
|
|
2016
|
+
logger.info('Your previous trial expired or was already claimed elsewhere. Starting a new one.');
|
|
2017
|
+
await deleteAnonSessionFile(screenciDir);
|
|
2018
|
+
const freshToken = await getOrCreateAnonToken(screenciDir);
|
|
2019
|
+
return { credential: anonCredential(freshToken), usedAnonCredential: true };
|
|
2020
|
+
}
|
|
2021
|
+
return { credential: anonCredential(token), usedAnonCredential: true };
|
|
2022
|
+
}
|
|
1984
2023
|
// Uploads the recordings already written under `.screenci` for the resolved
|
|
1985
2024
|
// config. Shared by `record` (after a Playwright run) and `retry` (which
|
|
1986
2025
|
// re-sends the existing recordings without re-running Playwright). A non-null
|
|
@@ -2001,17 +2040,17 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2001
2040
|
: resolve(dirname(resolvedConfigPath), '.env'), true);
|
|
2002
2041
|
const apiUrl = getDevBackendUrl();
|
|
2003
2042
|
const appUrl = getDevFrontendUrl();
|
|
2004
|
-
const secret = process.env.SCREENCI_SECRET;
|
|
2005
2043
|
const uploadPolicy = resolveRecordUploadPolicy(screenciConfig);
|
|
2006
2044
|
const configDir = dirname(resolvedConfigPath);
|
|
2007
2045
|
const screenciDir = resolve(configDir, '.screenci');
|
|
2046
|
+
const envFilePath = screenciConfig.envFile
|
|
2047
|
+
? resolve(configDir, screenciConfig.envFile)
|
|
2048
|
+
: resolve(configDir, '.env');
|
|
2008
2049
|
const completedRecordingCount = await countCompletedRecordings(screenciDir);
|
|
2050
|
+
const { credential, usedAnonCredential } = await resolveUploadCredential(screenciDir, apiUrl, envFilePath, process.env.SCREENCI_SECRET);
|
|
2009
2051
|
if (playwrightFailure !== null && completedRecordingCount === 0) {
|
|
2010
2052
|
logger.info('All recordings failed.');
|
|
2011
2053
|
}
|
|
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
2054
|
else if (playwrightFailure !== null &&
|
|
2016
2055
|
uploadPolicy === 'all-or-nothing') {
|
|
2017
2056
|
logger.info('Some recordings failed, skipping upload because record.upload is "all-or-nothing".');
|
|
@@ -2032,7 +2071,7 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2032
2071
|
plan: null,
|
|
2033
2072
|
};
|
|
2034
2073
|
try {
|
|
2035
|
-
uploadResult = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl,
|
|
2074
|
+
uploadResult = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, credential, undefined, verbose);
|
|
2036
2075
|
}
|
|
2037
2076
|
catch (err) {
|
|
2038
2077
|
if (isUploadCancelledError(err)) {
|
|
@@ -2075,6 +2114,9 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
|
|
|
2075
2114
|
: 'Recording finished, rendering in progress. Results available at:');
|
|
2076
2115
|
logger.info(pc.cyan(projectUrl));
|
|
2077
2116
|
}
|
|
2117
|
+
if (usedAnonCredential && (recordId !== null || projectId !== null)) {
|
|
2118
|
+
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).');
|
|
2119
|
+
}
|
|
2078
2120
|
if (notices.length > 0) {
|
|
2079
2121
|
logger.info('');
|
|
2080
2122
|
for (const notice of notices) {
|
|
@@ -2406,6 +2448,11 @@ function validateArgs(args) {
|
|
|
2406
2448
|
* code seeds, and a recording is never blocked by this fetch.
|
|
2407
2449
|
*/
|
|
2408
2450
|
async function fetchTextOverridesEnv(configPath, languages, verbose) {
|
|
2451
|
+
// Studio config only exists for a real (non-anonymous) org, and record must
|
|
2452
|
+
// still work without an account, so skip the fetch entirely rather than
|
|
2453
|
+
// calling requireScreenCISecret, which would hard-exit without a secret.
|
|
2454
|
+
if (!process.env.SCREENCI_SECRET)
|
|
2455
|
+
return {};
|
|
2409
2456
|
try {
|
|
2410
2457
|
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
2411
2458
|
const params = new URLSearchParams({
|
|
@@ -2446,6 +2493,11 @@ async function fetchTextOverridesEnv(configPath, languages, verbose) {
|
|
|
2446
2493
|
* is never blocked by this fetch.
|
|
2447
2494
|
*/
|
|
2448
2495
|
async function fetchRecordOptionsEnv(configPath, verbose) {
|
|
2496
|
+
// Studio config only exists for a real (non-anonymous) org, and record must
|
|
2497
|
+
// still work without an account, so skip the fetch entirely rather than
|
|
2498
|
+
// calling requireScreenCISecret, which would hard-exit without a secret.
|
|
2499
|
+
if (!process.env.SCREENCI_SECRET)
|
|
2500
|
+
return {};
|
|
2449
2501
|
try {
|
|
2450
2502
|
const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
|
|
2451
2503
|
const params = new URLSearchParams({
|
|
@@ -2480,9 +2532,10 @@ async function run(command, additionalArgs, customConfigPath, verbose = false, m
|
|
|
2480
2532
|
if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
|
|
2481
2533
|
await loadEnvFileFromConfigSource(configPath, false);
|
|
2482
2534
|
}
|
|
2483
|
-
// Only validate args for record command
|
|
2535
|
+
// Only validate args for record command. No secret is required here: record
|
|
2536
|
+
// can upload anonymously (see resolveUploadCredential), so this must not
|
|
2537
|
+
// hard-exit the way requireScreenCISecret does for account-only commands.
|
|
2484
2538
|
if (command === 'record') {
|
|
2485
|
-
await requireScreenCISecret(configPath);
|
|
2486
2539
|
validateArgs(additionalArgs);
|
|
2487
2540
|
const screenciDir = resolve(dirname(configPath), '.screenci');
|
|
2488
2541
|
clearRecordingDirectories(screenciDir);
|