screenci 0.0.83 → 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 (91) hide show
  1. package/dist/cli.d.ts +16 -3
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +138 -32
  4. package/dist/cli.js.map +1 -1
  5. package/dist/docs/video-sources/camera-and-zooming.screenci.js +1 -2
  6. package/dist/docs/video-sources/camera-and-zooming.screenci.js.map +1 -1
  7. package/dist/docs/video-sources/installation.screenci.js +1 -2
  8. package/dist/docs/video-sources/installation.screenci.js.map +1 -1
  9. package/dist/docs/video-sources/languages.screenci.js +3 -1
  10. package/dist/docs/video-sources/languages.screenci.js.map +1 -1
  11. package/dist/docs/video-sources/locator-highlight-animated.screenci.js +0 -1
  12. package/dist/docs/video-sources/locator-highlight-animated.screenci.js.map +1 -1
  13. package/dist/docs/video-sources/locator-highlight-still.screenci.js +0 -1
  14. package/dist/docs/video-sources/locator-highlight-still.screenci.js.map +1 -1
  15. package/dist/docs/video-sources/narration.screenci.js +3 -1
  16. package/dist/docs/video-sources/narration.screenci.js.map +1 -1
  17. package/dist/docs/video-sources/public-urls-and-embeds.screenci.js +3 -1
  18. package/dist/docs/video-sources/public-urls-and-embeds.screenci.js.map +1 -1
  19. package/dist/docs/video-sources/speaking-style.screenci.js +79 -14
  20. package/dist/docs/video-sources/speaking-style.screenci.js.map +1 -1
  21. package/dist/docs/video-sources/studio.screenci.js +3 -1
  22. package/dist/docs/video-sources/studio.screenci.js.map +1 -1
  23. package/dist/docs/video-sources/vertical-video.screenci.js +1 -1
  24. package/dist/src/anonSession.d.ts +40 -0
  25. package/dist/src/anonSession.d.ts.map +1 -0
  26. package/dist/src/anonSession.js +91 -0
  27. package/dist/src/anonSession.js.map +1 -0
  28. package/dist/src/asset.d.ts +11 -1
  29. package/dist/src/asset.d.ts.map +1 -1
  30. package/dist/src/asset.js +122 -52
  31. package/dist/src/asset.js.map +1 -1
  32. package/dist/src/audio.d.ts.map +1 -1
  33. package/dist/src/audio.js +2 -2
  34. package/dist/src/audio.js.map +1 -1
  35. package/dist/src/cue.d.ts.map +1 -1
  36. package/dist/src/cue.js +12 -4
  37. package/dist/src/cue.js.map +1 -1
  38. package/dist/src/disableAnimations.d.ts +33 -0
  39. package/dist/src/disableAnimations.d.ts.map +1 -0
  40. package/dist/src/disableAnimations.js +50 -0
  41. package/dist/src/disableAnimations.js.map +1 -0
  42. package/dist/src/events.d.ts +5 -1
  43. package/dist/src/events.d.ts.map +1 -1
  44. package/dist/src/events.js +4 -0
  45. package/dist/src/events.js.map +1 -1
  46. package/dist/src/git.d.ts +2 -0
  47. package/dist/src/git.d.ts.map +1 -1
  48. package/dist/src/git.js +1 -1
  49. package/dist/src/git.js.map +1 -1
  50. package/dist/src/init.d.ts +30 -8
  51. package/dist/src/init.d.ts.map +1 -1
  52. package/dist/src/init.js +112 -57
  53. package/dist/src/init.js.map +1 -1
  54. package/dist/src/instrument.d.ts.map +1 -1
  55. package/dist/src/instrument.js +5 -2
  56. package/dist/src/instrument.js.map +1 -1
  57. package/dist/src/linkSession.d.ts +13 -10
  58. package/dist/src/linkSession.d.ts.map +1 -1
  59. package/dist/src/linkSession.js +33 -29
  60. package/dist/src/linkSession.js.map +1 -1
  61. package/dist/src/missingAssetLog.d.ts +8 -0
  62. package/dist/src/missingAssetLog.d.ts.map +1 -0
  63. package/dist/src/missingAssetLog.js +16 -0
  64. package/dist/src/missingAssetLog.js.map +1 -0
  65. package/dist/src/recordingData.d.ts +1 -1
  66. package/dist/src/recordingData.d.ts.map +1 -1
  67. package/dist/src/runtimeContext.d.ts +17 -0
  68. package/dist/src/runtimeContext.d.ts.map +1 -1
  69. package/dist/src/runtimeContext.js +3 -0
  70. package/dist/src/runtimeContext.js.map +1 -1
  71. package/dist/src/runtimeMode.d.ts.map +1 -1
  72. package/dist/src/runtimeMode.js +8 -0
  73. package/dist/src/runtimeMode.js.map +1 -1
  74. package/dist/src/screenshot.d.ts.map +1 -1
  75. package/dist/src/screenshot.js +7 -0
  76. package/dist/src/screenshot.js.map +1 -1
  77. package/dist/src/types.d.ts +37 -1
  78. package/dist/src/types.d.ts.map +1 -1
  79. package/dist/src/types.js.map +1 -1
  80. package/dist/src/video.d.ts.map +1 -1
  81. package/dist/src/video.js +7 -0
  82. package/dist/src/video.js.map +1 -1
  83. package/dist/tsconfig.tsbuildinfo +1 -1
  84. package/package.json +1 -1
  85. package/skills/screenci/SKILL.md +10 -9
  86. package/skills/screenci/references/init.md +5 -6
  87. package/skills/screenci/references/record.md +9 -7
  88. package/dist/docs/video-sources/landing.screenci.d.ts +0 -2
  89. package/dist/docs/video-sources/landing.screenci.d.ts.map +0 -1
  90. package/dist/docs/video-sources/landing.screenci.js +0 -34
  91. package/dist/docs/video-sources/landing.screenci.js.map +0 -1
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<{
@@ -26,7 +28,7 @@ export declare function detectInteractiveSession(env?: NodeJS.ProcessEnv, stdout
26
28
  }): boolean;
27
29
  export declare function collectPlaywrightListTitles(suites: readonly PlaywrightListReportSuite[]): string[];
28
30
  /** What an uploaded file actually is, used to label it in the upload log. */
29
- type UploadAssetKind = 'overlay' | 'audio' | 'clip' | 'voice';
31
+ type UploadAssetKind = 'overlay' | 'audio' | 'clip' | 'voice' | 'cursor';
30
32
  type PreparedUploadAsset = {
31
33
  kind: UploadAssetKind;
32
34
  fileHash: string;
@@ -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":"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,EAGlB,MAAM,sBAAsB,CAAA;AAG7B,OAAO,EACL,KAAK,aAAa,EAMnB,MAAM,sBAAsB,CAAA;AAI7B,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,CAAA;AAKvE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAWvE,KAAK,yBAAyB,GAAG;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAChC,MAAM,CAAC,EAAE,yBAAyB,EAAE,CAAA;CACrC,CAAA;AAUD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,MAAM,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAmB,EAC5C,KAAK,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAkB,GACzC,OAAO,CAIT;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,SAAS,yBAAyB,EAAE,GAC3C,MAAM,EAAE,CAiBV;AA0ND,6EAA6E;AAC7E,KAAK,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;AAcxE,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,eAAe,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IAIpB,YAAY,CAAC,EAAE,OAAO,CAAA;IAMtB,YAAY,CAAC,EAAE,OAAO,CAAA;IAItB,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B,CAAA;AASD,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,CAAA;AAEjE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,gBAAgB,CAAA;CACzB,CAAA;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,MAAM,CAIR;AAED,KAAK,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAA;AAsE9C,wBAAsB,eAAe,CAAC,CAAC,EACrC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,WAAW,SAAI,GACd,OAAO,CAAC,CAAC,CAAC,CAWZ;AA0XD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,GAAG,KAAK,GAAG,OAAO,CAAC,EACtD,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI,GACxC,MAAM,IAAI,CAiBZ;AAwOD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAU3D;AA4MD,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,aAAa,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAmPhC;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,QAAQ,GAAG,uBAAuB,EACzC,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,QAAQ,GAAG,uBAAuB,CAYpC;AAED,wBAAgB,oCAAoC,CAClD,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,mBAAmB,EAAE,GAC5B,aAAa,CAoHf;AAQD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAWhE;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,YAAY,EAAE,MAAM,GACnB,OAAO,CAUT;AAED,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,MAAM,CAeR;AAKD,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,MAAM,CASR;AAED,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,IAAI,CAcN;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAM1D;AAID,KAAK,kBAAkB,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAAE,QAAQ,EAAE,kBAAkB,EAAE,CAAA;CAAE,CAAA;AAEtE;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,mBAAmB,EAAE,EAC7B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,aAAa,EACzB,MAAM,EAAE,WAAW,EACnB,gBAAgB,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACrD,OAAO,CAAC,mBAAmB,EAAE,CAAC,CA0DhC;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,mBAAmB,EAAE,GAChC,MAAM,CASR;AAiID,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,aAAa,EACzB,aAAa,CAAC,EAAE,MAAM,EACtB,OAAO,UAAQ,GACd,OAAO,CAAC;IACT,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,OAAO,CAAA;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,mBAAmB,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAClE,aAAa,EAAE,kBAAkB,EAAE,CAAA;IACnC,0BAA0B,EAAE,MAAM,EAAE,CAAA;IACpC,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;CACrB,CAAC,CAkID;AA4KD,wBAAsB,4BAA4B,CAChD,kBAAkB,EAAE,MAAM,GACzB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAU7B;AAWD,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,aAAa,GAAG,SAAS,GAClC,MAAM,GAAG,SAAS,CAepB;AAED,wBAAgB,gCAAgC,CAC9C,YAAY,EAAE,MAAM,GACnB,kBAAkB,GAAG,SAAS,CAmBhC;AAED,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,GACnB,OAAO,GAAG,SAAS,CAQrB;AAkCD,wBAAgB,wBAAwB,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAS3E;AAiCD,wBAAsB,qBAAqB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxE,kBAAkB,EAAE,MAAM,CAAA;IAC1B,cAAc,EAAE,cAAc,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf,CAAC,CAqBD;AAkFD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAY9D;AAuHD;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,GAAG,SAAS,GAChC,OAAO,CAAC;IAAE,UAAU,EAAE,aAAa,CAAC;IAAC,kBAAkB,EAAE,OAAO,CAAA;CAAE,CAAC,CAiCrE;AAgND,wBAAsB,IAAI,kBAgQzB;AAQD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAClD,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB,CAgDA;AA+TD,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAchD"}
package/dist/cli.js CHANGED
@@ -10,16 +10,22 @@ import { Command, CommanderError } from 'commander';
10
10
  import { confirm } from '@inquirer/prompts';
11
11
  import pc from 'picocolors';
12
12
  import { logger } from './src/logger.js';
13
- import { determinePackageManager, parsePackageManager, runInit, } from './src/init.js';
13
+ import { determinePackageManager, initToggleOptionsFromCommander, parsePackageManager, registerInitToggleOptions, runInit, } from './src/init.js';
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
@@ -218,6 +224,8 @@ const UPLOAD_ASSET_LABEL = {
218
224
  // (e.g. via `narration({ en: { intro: { media } } })`).
219
225
  clip: 'Narration',
220
226
  voice: 'Voice',
227
+ // A custom cursor image set via `renderOptions.mouse.image`.
228
+ cursor: 'Cursor',
221
229
  };
222
230
  export function formatStudioUrl(appUrl, projectId, videoId) {
223
231
  // The video hub resolves `?studio` to the right language page and scrolls it
@@ -346,7 +354,7 @@ async function loadUploadCandidate(screenciDir, entry, verbose) {
346
354
  preparedUploadAssets,
347
355
  };
348
356
  }
349
- 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) {
350
358
  const { entry, videoName, data: rawData, preparedUploadAssets } = candidate;
351
359
  let projectId = null;
352
360
  let videoId = null;
@@ -377,7 +385,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
377
385
  // for overlays, no hash. Recover their identity from a previous upload of
378
386
  // this video before starting, so the recording data references them by hash
379
387
  // and the backend existence check confirms they are still stored.
380
- 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);
381
389
  if (unresolved.length > 0) {
382
390
  progressReporter.complete(progressIndex, 'failure');
383
391
  return {
@@ -395,7 +403,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
395
403
  method: 'POST',
396
404
  headers: {
397
405
  'Content-Type': 'application/json',
398
- 'X-ScreenCI-Secret': secret,
406
+ [credential.header]: credential.value,
399
407
  },
400
408
  body: JSON.stringify({
401
409
  projectName,
@@ -438,7 +446,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
438
446
  videoId: null,
439
447
  hadFailure: true,
440
448
  videoName,
441
- failureMessage: formatUploadStartFailureMessage(videoName, startResponse.status, text, secret),
449
+ failureMessage: formatUploadStartFailureMessage(videoName, startResponse.status, text, credential.value),
442
450
  recordId,
443
451
  };
444
452
  }
@@ -463,7 +471,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
463
471
  logger.info(`recordingId=${recordingId} projectId=${projectId}`);
464
472
  logger.info(`assets=${preparedUploadAssets.length} recordingHash=${recordingHash ?? 'none'}`);
465
473
  }
466
- await uploadAssets(preparedUploadAssets, apiUrl, secret, recordingId, uploadAbort.signal, uploadAbort.throwIfAborted, progressReporter);
474
+ await uploadAssets(preparedUploadAssets, apiUrl, credential, recordingId, uploadAbort.signal, uploadAbort.throwIfAborted, progressReporter);
467
475
  uploadAbort.throwIfAborted();
468
476
  const fileStat = await stat(recordingPath);
469
477
  if (verbose) {
@@ -481,7 +489,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
481
489
  headers: {
482
490
  'Content-Type': recordingContentType,
483
491
  'Content-Length': String(fileStat.size),
484
- 'X-ScreenCI-Secret': secret,
492
+ [credential.header]: credential.value,
485
493
  },
486
494
  body: stream,
487
495
  signal: uploadAbort.signal,
@@ -501,7 +509,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
501
509
  videoId,
502
510
  hadFailure: true,
503
511
  videoName,
504
- 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)}`,
505
513
  recordId,
506
514
  ...(plan !== null && { plan }),
507
515
  };
@@ -1095,6 +1103,37 @@ export async function collectUploadAssets(data, configDir) {
1095
1103
  for (const asset of await prepareCustomVoiceAssets(data, configDir)) {
1096
1104
  assets.set(`path:${asset.path}`, asset);
1097
1105
  }
1106
+ // Custom cursor image (`renderOptions.mouse.image`). Unlike overlays it is not
1107
+ // referenced by a timeline event, so it is collected here. It is a local path
1108
+ // only before upload; a `{ assetPath, fileHash }` value has already been
1109
+ // uploaded (e.g. re-running the CLI on annotated data) and needs no work.
1110
+ const cursorImage = data.renderOptions?.mouse?.image;
1111
+ if (typeof cursorImage === 'string' && !assets.has(`path:${cursorImage}`)) {
1112
+ const resolvedFile = await readRecordingFile(cursorImage, configDir, sourceFilePath);
1113
+ if (resolvedFile === null) {
1114
+ // The local file is gone (e.g. gitignored on CI). Reference it by path so
1115
+ // its identity can be recovered from a previous upload of this video.
1116
+ assets.set(`path:${cursorImage}`, {
1117
+ kind: 'cursor',
1118
+ fileHash: '',
1119
+ path: cursorImage,
1120
+ size: 0,
1121
+ needsResolve: true,
1122
+ });
1123
+ }
1124
+ else {
1125
+ assets.set(`path:${cursorImage}`, {
1126
+ kind: 'cursor',
1127
+ fileHash: createHash('sha256')
1128
+ .update(resolvedFile.buffer)
1129
+ .digest('hex'),
1130
+ path: cursorImage,
1131
+ size: resolvedFile.buffer.byteLength,
1132
+ fileBuffer: resolvedFile.buffer,
1133
+ contentType: contentTypeForPath(resolvedFile.resolvedPath),
1134
+ });
1135
+ }
1136
+ }
1098
1137
  return [...assets.values()];
1099
1138
  }
1100
1139
  export function stripVoicePath(voice, byPath) {
@@ -1122,8 +1161,26 @@ export function annotateRecordingDataWithAssetHashes(data, assets) {
1122
1161
  byName.set(asset.name, asset.fileHash);
1123
1162
  byPath.set(asset.path, asset.fileHash);
1124
1163
  }
1164
+ // Rewrite a custom cursor image (`renderOptions.mouse.image`) from its local
1165
+ // path to `{ assetPath, fileHash }` so the renderer can resolve it by content
1166
+ // hash. Leave it untouched when it is already an object or has no known hash.
1167
+ const renderOptions = (() => {
1168
+ const ro = data.renderOptions;
1169
+ const mouse = ro?.mouse;
1170
+ const image = mouse?.image;
1171
+ if (mouse === undefined || typeof image !== 'string')
1172
+ return ro;
1173
+ const fileHash = byPath.get(image);
1174
+ if (fileHash === undefined)
1175
+ return ro;
1176
+ return {
1177
+ ...ro,
1178
+ mouse: { ...mouse, image: { assetPath: image, fileHash } },
1179
+ };
1180
+ })();
1125
1181
  return {
1126
1182
  ...data,
1183
+ renderOptions,
1127
1184
  events: data.events.map((event) => {
1128
1185
  if (event.type === 'assetStart') {
1129
1186
  if ('studio' in event || 'dependency' in event)
@@ -1296,7 +1353,7 @@ export function displayAssetPath(assetPath) {
1296
1353
  * Returns the assets that could not be resolved (no previous version), so the
1297
1354
  * caller can fail the recording with actionable guidance.
1298
1355
  */
1299
- export async function resolveMissingUploadAssets(assets, projectName, videoName, apiUrl, secret, signal, progressReporter) {
1356
+ export async function resolveMissingUploadAssets(assets, projectName, videoName, apiUrl, credential, signal, progressReporter) {
1300
1357
  const pending = assets.filter((asset) => asset.needsResolve === true);
1301
1358
  if (pending.length === 0)
1302
1359
  return [];
@@ -1309,14 +1366,14 @@ export async function resolveMissingUploadAssets(assets, projectName, videoName,
1309
1366
  method: 'POST',
1310
1367
  headers: {
1311
1368
  'Content-Type': 'application/json',
1312
- 'X-ScreenCI-Secret': secret,
1369
+ [credential.header]: credential.value,
1313
1370
  },
1314
1371
  body: JSON.stringify({ projectName, videoName, assets: refs }),
1315
1372
  signal,
1316
1373
  }), signal);
1317
1374
  if (!res.ok) {
1318
1375
  const text = await res.text();
1319
- 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)}`);
1320
1377
  }
1321
1378
  const body = (await res.json());
1322
1379
  const resolved = Array.isArray(body.resolved) ? body.resolved : [];
@@ -1359,7 +1416,7 @@ export function formatUnresolvedAssetMessage(videoName, unresolved) {
1359
1416
  'Record once with these files present so they are uploaded, or commit them so they are available here.',
1360
1417
  ].join('\n');
1361
1418
  }
1362
- async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIfAborted, progressReporter) {
1419
+ async function uploadAssets(assets, apiUrl, credential, recordingId, signal, throwIfAborted, progressReporter) {
1363
1420
  const logInfo = (message) => {
1364
1421
  if (progressReporter) {
1365
1422
  progressReporter.info(message);
@@ -1377,7 +1434,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
1377
1434
  method: 'POST',
1378
1435
  headers: {
1379
1436
  'Content-Type': 'application/json',
1380
- 'X-ScreenCI-Secret': secret,
1437
+ [credential.header]: credential.value,
1381
1438
  },
1382
1439
  body: JSON.stringify({
1383
1440
  fileHash: asset.fileHash,
@@ -1390,7 +1447,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
1390
1447
  }), signal);
1391
1448
  if (!checkRes.ok) {
1392
1449
  const text = await checkRes.text();
1393
- 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)}`);
1394
1451
  }
1395
1452
  const checkBody = (await checkRes.json());
1396
1453
  if (checkBody.exists) {
@@ -1420,7 +1477,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
1420
1477
  headers: {
1421
1478
  'Content-Type': contentType,
1422
1479
  'Content-Length': String(fileBuffer.byteLength),
1423
- 'X-ScreenCI-Secret': secret,
1480
+ [credential.header]: credential.value,
1424
1481
  'X-ScreenCI-File-Hash': asset.fileHash,
1425
1482
  'X-ScreenCI-Asset-Size': String(asset.size),
1426
1483
  'X-ScreenCI-Asset-Path': encodeURIComponent(asset.path),
@@ -1434,7 +1491,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
1434
1491
  logInfo(`${pc.green('✔')} ${label} already exists: ${displayAssetPath(asset.path)}`);
1435
1492
  }
1436
1493
  else {
1437
- 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)}`);
1438
1495
  }
1439
1496
  }
1440
1497
  else {
@@ -1452,7 +1509,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
1452
1509
  }
1453
1510
  }
1454
1511
  }
1455
- export async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specificEntry, verbose = false) {
1512
+ export async function uploadRecordings(screenciDir, projectName, apiUrl, credential, specificEntry, verbose = false) {
1456
1513
  const uploadAbort = createUploadAbortController('upload');
1457
1514
  const recordId = randomUUID();
1458
1515
  let entries;
@@ -1500,7 +1557,7 @@ export async function uploadRecordings(screenciDir, projectName, apiUrl, secret,
1500
1557
  // waits for all of them to land before dispatching the batch, so it needs
1501
1558
  // to know how many to expect.
1502
1559
  const screenshotCount = candidates.filter((candidate) => candidate.data.output === 'screenshot').length;
1503
- 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)));
1504
1561
  firstProjectId =
1505
1562
  results.find((result) => result.projectId !== null)?.projectId ?? null;
1506
1563
  const resolvedPlan = results.find((result) => result.plan !== undefined)?.plan ?? null;
@@ -1761,10 +1818,10 @@ export async function requireScreenCISecret(configPath) {
1761
1818
  const { resolvedConfigPath, screenciConfig } = await loadScreenCIConfigAndEnv(configPath);
1762
1819
  const secret = process.env.SCREENCI_SECRET;
1763
1820
  if (!secret) {
1764
- // No browser sign-in flow: the secret comes from an init one-time token or
1765
- // 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.
1766
1823
  const envFilePath = await resolveProjectEnvFilePath(resolvedConfigPath);
1767
- 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}.`);
1768
1825
  logScreenCISecretGuide();
1769
1826
  process.exit(1);
1770
1827
  }
@@ -1930,6 +1987,39 @@ async function printInfo(configPath) {
1930
1987
  const data = await res.json();
1931
1988
  process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
1932
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
+ }
1933
2023
  // Uploads the recordings already written under `.screenci` for the resolved
1934
2024
  // config. Shared by `record` (after a Playwright run) and `retry` (which
1935
2025
  // re-sends the existing recordings without re-running Playwright). A non-null
@@ -1950,17 +2040,17 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
1950
2040
  : resolve(dirname(resolvedConfigPath), '.env'), true);
1951
2041
  const apiUrl = getDevBackendUrl();
1952
2042
  const appUrl = getDevFrontendUrl();
1953
- const secret = process.env.SCREENCI_SECRET;
1954
2043
  const uploadPolicy = resolveRecordUploadPolicy(screenciConfig);
1955
2044
  const configDir = dirname(resolvedConfigPath);
1956
2045
  const screenciDir = resolve(configDir, '.screenci');
2046
+ const envFilePath = screenciConfig.envFile
2047
+ ? resolve(configDir, screenciConfig.envFile)
2048
+ : resolve(configDir, '.env');
1957
2049
  const completedRecordingCount = await countCompletedRecordings(screenciDir);
2050
+ const { credential, usedAnonCredential } = await resolveUploadCredential(screenciDir, apiUrl, envFilePath, process.env.SCREENCI_SECRET);
1958
2051
  if (playwrightFailure !== null && completedRecordingCount === 0) {
1959
2052
  logger.info('All recordings failed.');
1960
2053
  }
1961
- else if (!secret) {
1962
- logger.info(`No SCREENCI_SECRET configured for uploads. Copy your secret from ${getScreenCISecretsUrl()} into the project env file, then rerun ${getSuggestedScreenciCommand('record')}.`);
1963
- }
1964
2054
  else if (playwrightFailure !== null &&
1965
2055
  uploadPolicy === 'all-or-nothing') {
1966
2056
  logger.info('Some recordings failed, skipping upload because record.upload is "all-or-nothing".');
@@ -1981,7 +2071,7 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
1981
2071
  plan: null,
1982
2072
  };
1983
2073
  try {
1984
- uploadResult = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, secret, undefined, verbose);
2074
+ uploadResult = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, credential, undefined, verbose);
1985
2075
  }
1986
2076
  catch (err) {
1987
2077
  if (isUploadCancelledError(err)) {
@@ -2024,6 +2114,9 @@ async function uploadRecordedVideosForConfig(configPath, playwrightFailure, verb
2024
2114
  : 'Recording finished, rendering in progress. Results available at:');
2025
2115
  logger.info(pc.cyan(projectUrl));
2026
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
+ }
2027
2120
  if (notices.length > 0) {
2028
2121
  logger.info('');
2029
2122
  for (const notice of notices) {
@@ -2194,20 +2287,22 @@ export async function main() {
2194
2287
  await deleteVideoCommand(id, options['yes'] ?? false, options['config']);
2195
2288
  });
2196
2289
  // init command
2197
- program
2290
+ const initCommand = program
2198
2291
  .command('init [name]')
2199
2292
  .description('Initialize a new screenci project')
2200
2293
  .option('--agent <name>', 'target agent for skills install, e.g. opencode. Supported agents: https://github.com/vercel-labs/skills#supported-agents')
2201
2294
  .option('--package-manager <manager>', `package manager to use: npm, pnpm, or yarn 2+ (default: ${defaultPackageManager})`)
2202
2295
  .option('-y, --yes', 'accept init defaults')
2203
- .option('-v, --verbose', 'verbose output')
2204
- .action(async (name, options) => {
2296
+ .option('-v, --verbose', 'verbose output');
2297
+ registerInitToggleOptions(initCommand);
2298
+ initCommand.action(async (name, options) => {
2205
2299
  const agent = options['agent'];
2206
2300
  await runInit(name, {
2207
2301
  verbose: options['verbose'] ?? false,
2208
2302
  yes: options['yes'] ?? false,
2209
2303
  packageManager: parsePackageManager(options['packageManager'], process.env['SCREENCI_INIT_CWD'] ?? process.cwd()),
2210
2304
  ...(agent !== undefined ? { agent } : {}),
2305
+ ...initToggleOptionsFromCommander(options),
2211
2306
  });
2212
2307
  });
2213
2308
  try {
@@ -2353,6 +2448,11 @@ function validateArgs(args) {
2353
2448
  * code seeds, and a recording is never blocked by this fetch.
2354
2449
  */
2355
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 {};
2356
2456
  try {
2357
2457
  const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
2358
2458
  const params = new URLSearchParams({
@@ -2393,6 +2493,11 @@ async function fetchTextOverridesEnv(configPath, languages, verbose) {
2393
2493
  * is never blocked by this fetch.
2394
2494
  */
2395
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 {};
2396
2501
  try {
2397
2502
  const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath);
2398
2503
  const params = new URLSearchParams({
@@ -2427,9 +2532,10 @@ async function run(command, additionalArgs, customConfigPath, verbose = false, m
2427
2532
  if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
2428
2533
  await loadEnvFileFromConfigSource(configPath, false);
2429
2534
  }
2430
- // 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.
2431
2538
  if (command === 'record') {
2432
- await requireScreenCISecret(configPath);
2433
2539
  validateArgs(additionalArgs);
2434
2540
  const screenciDir = resolve(dirname(configPath), '.screenci');
2435
2541
  clearRecordingDirectories(screenciDir);