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.
Files changed (46) hide show
  1. package/dist/cli.d.ts +29 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +113 -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 +60 -0
  9. package/dist/src/anonSession.d.ts.map +1 -0
  10. package/dist/src/anonSession.js +115 -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 +32 -70
  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,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":"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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 { 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, 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
  };
@@ -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, secret, signal, progressReporter) {
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
- 'X-ScreenCI-Secret': secret,
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, secret)}`);
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, secret, recordingId, signal, throwIfAborted, progressReporter) {
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
- 'X-ScreenCI-Secret': secret,
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, secret)}`);
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
- 'X-ScreenCI-Secret': secret,
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, secret)}`);
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, secret, specificEntry, verbose = false) {
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, secret, verbose, uploadAbort, progressReporter, index, recordId, screenshotCount)));
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
- // 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.
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}, or re-run init with a one-time setup token from ${pc.cyan(getScreenCISecretsUrl())}.`);
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, secret, undefined, verbose);
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