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.
Files changed (46) hide show
  1. package/dist/cli.d.ts +15 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +81 -28
  4. package/dist/cli.js.map +1 -1
  5. package/dist/docs/video-sources/speaking-style.screenci.js +79 -14
  6. package/dist/docs/video-sources/speaking-style.screenci.js.map +1 -1
  7. package/dist/docs/video-sources/vertical-video.screenci.js +1 -1
  8. package/dist/src/anonSession.d.ts +40 -0
  9. package/dist/src/anonSession.d.ts.map +1 -0
  10. package/dist/src/anonSession.js +91 -0
  11. package/dist/src/anonSession.js.map +1 -0
  12. package/dist/src/asset.d.ts.map +1 -1
  13. package/dist/src/asset.js +2 -2
  14. package/dist/src/asset.js.map +1 -1
  15. package/dist/src/audio.d.ts.map +1 -1
  16. package/dist/src/audio.js +2 -2
  17. package/dist/src/audio.js.map +1 -1
  18. package/dist/src/cue.d.ts.map +1 -1
  19. package/dist/src/cue.js +12 -4
  20. package/dist/src/cue.js.map +1 -1
  21. package/dist/src/events.d.ts +5 -1
  22. package/dist/src/events.d.ts.map +1 -1
  23. package/dist/src/events.js.map +1 -1
  24. package/dist/src/git.d.ts +2 -0
  25. package/dist/src/git.d.ts.map +1 -1
  26. package/dist/src/git.js +1 -1
  27. package/dist/src/git.js.map +1 -1
  28. package/dist/src/init.d.ts +6 -8
  29. package/dist/src/init.d.ts.map +1 -1
  30. package/dist/src/init.js +24 -45
  31. package/dist/src/init.js.map +1 -1
  32. package/dist/src/linkSession.d.ts +0 -21
  33. package/dist/src/linkSession.d.ts.map +1 -1
  34. package/dist/src/linkSession.js +4 -57
  35. package/dist/src/linkSession.js.map +1 -1
  36. package/dist/src/missingAssetLog.d.ts +8 -0
  37. package/dist/src/missingAssetLog.d.ts.map +1 -0
  38. package/dist/src/missingAssetLog.js +16 -0
  39. package/dist/src/missingAssetLog.js.map +1 -0
  40. package/dist/src/recordingData.d.ts +1 -1
  41. package/dist/src/recordingData.d.ts.map +1 -1
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/skills/screenci/SKILL.md +10 -9
  45. package/skills/screenci/references/init.md +5 -7
  46. 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, secret: string, signal: AbortSignal, progressReporter?: {
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, secret: string, specificEntry?: string, verbose?: boolean): Promise<{
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":"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"}
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, secret, verbose, uploadAbort, progressReporter, progressIndex, recordId, expectedScreenshotCount) {
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, secret, uploadAbort.signal, progressReporter);
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
- 'X-ScreenCI-Secret': secret,
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, secret),
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, secret, recordingId, uploadAbort.signal, uploadAbort.throwIfAborted, progressReporter);
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
- 'X-ScreenCI-Secret': secret,
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, secret)}`,
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, secret, signal, progressReporter) {
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
- 'X-ScreenCI-Secret': secret,
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, secret)}`);
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, secret, recordingId, signal, throwIfAborted, progressReporter) {
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
- 'X-ScreenCI-Secret': secret,
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, secret)}`);
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
- 'X-ScreenCI-Secret': secret,
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, secret)}`);
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, secret, specificEntry, verbose = false) {
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, secret, verbose, uploadAbort, progressReporter, index, recordId, screenshotCount)));
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
- // No browser sign-in flow: the secret comes from an init one-time token or
1816
- // from the secrets page. Guide the user to either path and exit non-zero.
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}, or re-run init with a one-time setup token from ${pc.cyan(getScreenCISecretsUrl())}.`);
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, secret, undefined, verbose);
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);