screenci 0.0.71 → 0.0.73

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 (113) hide show
  1. package/README.md +13 -13
  2. package/dist/cli.d.ts +14 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +257 -81
  5. package/dist/cli.js.map +1 -1
  6. package/dist/docs/manifest.d.ts +3 -3
  7. package/dist/docs/manifest.js +1 -1
  8. package/dist/docs/manifest.js.map +1 -1
  9. package/dist/docs/video-sources/installation.screenci.js +13 -12
  10. package/dist/docs/video-sources/installation.screenci.js.map +1 -1
  11. package/dist/docs/video-sources/landing.screenci.js +16 -9
  12. package/dist/docs/video-sources/landing.screenci.js.map +1 -1
  13. package/dist/docs/video-sources/public-urls.screenci.js +16 -9
  14. package/dist/docs/video-sources/public-urls.screenci.js.map +1 -1
  15. package/dist/docs/video-sources/studio.screenci.js +42 -27
  16. package/dist/docs/video-sources/studio.screenci.js.map +1 -1
  17. package/dist/index.d.ts +7 -5
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/src/asset.d.ts +82 -51
  22. package/dist/src/asset.d.ts.map +1 -1
  23. package/dist/src/asset.js +93 -46
  24. package/dist/src/asset.js.map +1 -1
  25. package/dist/src/audio.d.ts +6 -24
  26. package/dist/src/audio.d.ts.map +1 -1
  27. package/dist/src/audio.js +10 -35
  28. package/dist/src/audio.js.map +1 -1
  29. package/dist/src/builder.d.ts +214 -0
  30. package/dist/src/builder.d.ts.map +1 -0
  31. package/dist/src/builder.js +212 -0
  32. package/dist/src/builder.js.map +1 -0
  33. package/dist/src/config.d.ts +1 -1
  34. package/dist/src/config.d.ts.map +1 -1
  35. package/dist/src/config.js +13 -7
  36. package/dist/src/config.js.map +1 -1
  37. package/dist/src/cue.d.ts +40 -123
  38. package/dist/src/cue.d.ts.map +1 -1
  39. package/dist/src/cue.js +190 -30
  40. package/dist/src/cue.js.map +1 -1
  41. package/dist/src/defaults.d.ts +2 -2
  42. package/dist/src/defaults.d.ts.map +1 -1
  43. package/dist/src/defaults.js +2 -2
  44. package/dist/src/defaults.js.map +1 -1
  45. package/dist/src/events.d.ts +80 -15
  46. package/dist/src/events.d.ts.map +1 -1
  47. package/dist/src/events.js +72 -6
  48. package/dist/src/events.js.map +1 -1
  49. package/dist/src/init.d.ts.map +1 -1
  50. package/dist/src/init.js +80 -39
  51. package/dist/src/init.js.map +1 -1
  52. package/dist/src/locales.d.ts +19 -0
  53. package/dist/src/locales.d.ts.map +1 -0
  54. package/dist/src/locales.js +60 -0
  55. package/dist/src/locales.js.map +1 -0
  56. package/dist/src/localize.d.ts +135 -0
  57. package/dist/src/localize.d.ts.map +1 -0
  58. package/dist/src/localize.js +195 -0
  59. package/dist/src/localize.js.map +1 -0
  60. package/dist/src/localizeRuntime.d.ts +53 -0
  61. package/dist/src/localizeRuntime.d.ts.map +1 -0
  62. package/dist/src/localizeRuntime.js +79 -0
  63. package/dist/src/localizeRuntime.js.map +1 -0
  64. package/dist/src/mouse.d.ts.map +1 -1
  65. package/dist/src/mouse.js +6 -0
  66. package/dist/src/mouse.js.map +1 -1
  67. package/dist/src/recordingData.d.ts +5 -4
  68. package/dist/src/recordingData.d.ts.map +1 -1
  69. package/dist/src/runtimeContext.d.ts +22 -4
  70. package/dist/src/runtimeContext.d.ts.map +1 -1
  71. package/dist/src/runtimeContext.js +11 -0
  72. package/dist/src/runtimeContext.js.map +1 -1
  73. package/dist/src/runtimeMode.d.ts +47 -0
  74. package/dist/src/runtimeMode.d.ts.map +1 -1
  75. package/dist/src/runtimeMode.js +124 -0
  76. package/dist/src/runtimeMode.js.map +1 -1
  77. package/dist/src/screenshot.d.ts +56 -2
  78. package/dist/src/screenshot.d.ts.map +1 -1
  79. package/dist/src/screenshot.js +49 -10
  80. package/dist/src/screenshot.js.map +1 -1
  81. package/dist/src/stillCapture.d.ts +10 -4
  82. package/dist/src/stillCapture.d.ts.map +1 -1
  83. package/dist/src/stillCapture.js +11 -0
  84. package/dist/src/stillCapture.js.map +1 -1
  85. package/dist/src/studio.d.ts +55 -22
  86. package/dist/src/studio.d.ts.map +1 -1
  87. package/dist/src/studio.js +38 -12
  88. package/dist/src/studio.js.map +1 -1
  89. package/dist/src/types.d.ts +20 -7
  90. package/dist/src/types.d.ts.map +1 -1
  91. package/dist/src/types.js +1 -1
  92. package/dist/src/types.js.map +1 -1
  93. package/dist/src/video.d.ts +149 -13
  94. package/dist/src/video.d.ts.map +1 -1
  95. package/dist/src/video.js +98 -25
  96. package/dist/src/video.js.map +1 -1
  97. package/dist/src/voiceConfig.d.ts +110 -0
  98. package/dist/src/voiceConfig.d.ts.map +1 -0
  99. package/dist/src/voiceConfig.js +2 -0
  100. package/dist/src/voiceConfig.js.map +1 -0
  101. package/dist/src/voices.d.ts +27 -23
  102. package/dist/src/voices.d.ts.map +1 -1
  103. package/dist/src/voices.js +7 -4
  104. package/dist/src/voices.js.map +1 -1
  105. package/dist/tsconfig.tsbuildinfo +1 -1
  106. package/package.json +1 -1
  107. package/skills/screenci/SKILL.md +51 -47
  108. package/skills/screenci/references/init.md +1 -1
  109. package/skills/screenci/references/record.md +6 -6
  110. package/dist/docs/video-sources/product-pitch.screenci.d.ts +0 -2
  111. package/dist/docs/video-sources/product-pitch.screenci.d.ts.map +0 -1
  112. package/dist/docs/video-sources/product-pitch.screenci.js +0 -136
  113. package/dist/docs/video-sources/product-pitch.screenci.js.map +0 -1
package/README.md CHANGED
@@ -41,7 +41,7 @@ already know Playwright locators, navigation, and waiting, you already know
41
41
  most of the automation layer.
42
42
 
43
43
  ```ts
44
- // videos/onboarding.screenci.ts
44
+ // recordings/onboarding.screenci.ts
45
45
  import { hide, speed, time, video } from 'screenci'
46
46
 
47
47
  video('Onboarding flow', async ({ page }) => {
@@ -71,18 +71,18 @@ but remap its rendered duration.
71
71
 
72
72
  ## Authoring helpers
73
73
 
74
- | Export | What it does |
75
- | ----------------- | -------------------------------------------------------- |
76
- | `defineConfig` | Wraps Playwright config with ScreenCI defaults |
77
- | `video` | Declares a video recording test |
78
- | `createNarration` | Creates typed narration controllers |
79
- | `hide` | Cuts setup or cleanup out of the visible recording |
80
- | `autoZoom` | Smooth camera follow for an interaction block |
81
- | `zoomTo` | Manual camera framing for a locator or point |
82
- | `resetZoom` | Returns from manual framing to the full viewport |
83
- | `createOverlays` | Inserts timed media overlays into the recording timeline |
84
- | `voices` | Available voice constants such as `voices.Ava` |
85
- | `modelTypes` | Narration model constants |
74
+ | Export | What it does |
75
+ | ---------------- | ------------------------------------------------------------ |
76
+ | `defineConfig` | Wraps Playwright config with ScreenCI defaults |
77
+ | `video` | Declares a video recording test |
78
+ | `video.localize` | Declares localized narration / text and records per language |
79
+ | `hide` | Cuts setup or cleanup out of the visible recording |
80
+ | `autoZoom` | Smooth camera follow for an interaction block |
81
+ | `zoomTo` | Manual camera framing for a locator or point |
82
+ | `resetZoom` | Returns from manual framing to the full viewport |
83
+ | `createOverlays` | Inserts timed media overlays into the recording timeline |
84
+ | `voices` | Available voice constants such as `voices.Ava` |
85
+ | `modelTypes` | Narration model constants |
86
86
 
87
87
  See the [docs](https://screenci.com/docs) for configuration, narration,
88
88
  camera, and CI setup.
package/dist/cli.d.ts CHANGED
@@ -45,6 +45,9 @@ export type StudioUploadNotice = {
45
45
  };
46
46
  export declare function formatStudioUrl(appUrl: string, projectId: string, videoId: string): string;
47
47
  type OrgPlan = 'free' | 'starter' | 'business';
48
+ export declare class SignInTimeoutError extends Error {
49
+ constructor(message: string);
50
+ }
48
51
  export declare function withUploadRetry<T>(fn: () => Promise<T>, signal: AbortSignal | undefined, maxAttempts?: number): Promise<T>;
49
52
  export declare function attachUploadAbortStdinListener(input: Pick<NodeJS.ReadStream, 'on' | 'off' | 'pause'>, onAbort: (signal: NodeJS.Signals) => void): () => void;
50
53
  export declare function collectUploadAssets(data: RecordingData, configDir: string): Promise<PreparedUploadAsset[]>;
@@ -53,6 +56,7 @@ export declare function annotateRecordingDataWithAssetHashes(data: RecordingData
53
56
  export declare function formatUploadStartFailureMessage(videoName: string, status: number, responseText: string, secret: string): string;
54
57
  export declare function formatFailedVideoMessage(videoName: string, message: string): string;
55
58
  export declare function printUploadStartFailureMessage(videoName: string, status: number, responseText: string, secret: string): void;
59
+ export declare function displayAssetPath(assetPath: string): string;
56
60
  export declare function uploadRecordings(screenciDir: string, projectName: string, apiUrl: string, secret: string, specificEntry?: string, verbose?: boolean): Promise<{
57
61
  projectId: string | null;
58
62
  recordId: string | null;
@@ -73,7 +77,17 @@ export declare function extractGrep(args: string[]): string | undefined;
73
77
  export declare function ensureScreenciSecret(resolvedConfigPath?: string, opts?: {
74
78
  interactive?: boolean;
75
79
  pollAuth?: boolean;
80
+ noPollAuth?: boolean;
76
81
  }): Promise<string | undefined>;
77
82
  export declare function main(): Promise<void>;
83
+ export declare function parseRecordCliArgs(args: string[]): {
84
+ configPath: string | undefined;
85
+ verbose: boolean;
86
+ pollAuth: boolean;
87
+ noPollAuth: boolean;
88
+ remote: boolean;
89
+ languages: string | undefined;
90
+ otherArgs: string[];
91
+ };
78
92
  export declare function logCliError(error: unknown): void;
79
93
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EACV,uBAAuB,EACvB,aAAa,EAEd,MAAM,iBAAiB,CAAA;AAMxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAkB,MAAM,gBAAgB,CAAA;AAKxE,OAAO,EAGL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EAOlB,MAAM,sBAAsB,CAAA;AAQ7B,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,CAAA;AAqBvE,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;AAiND,KAAK,mBAAmB,GAAG;IACzB,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;CACrB,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,CAER;AAED,KAAK,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAA;AAkE9C,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;AAoTD,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;AAkaD,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,aAAa,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAiGhC;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,QAAQ,GAAG,uBAAuB,GACxC,QAAQ,GAAG,uBAAuB,CAKpC;AAED,wBAAgB,oCAAoC,CAClD,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,mBAAmB,EAAE,GAC5B,aAAa,CAqEf;AAQD,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;AA8GD,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,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,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;CACrB,CAAC,CA8GD;AA8LD,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;AAkGD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAY9D;AAqND,wBAAsB,oBAAoB,CACxC,kBAAkB,CAAC,EAAE,MAAM,EAC3B,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GACvD,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA8I7B;AAED,wBAAsB,IAAI,kBAoXzB;AAmOD,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAchD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -4,13 +4,13 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync,
4
4
  import { createHash, randomUUID } from 'crypto';
5
5
  import { createRequire } from 'module';
6
6
  import { appendFile, readdir, readFile, stat, writeFile } from 'fs/promises';
7
- import { delimiter, dirname, relative as pathRelative, resolve } from 'path';
7
+ import { delimiter, dirname, isAbsolute, relative as pathRelative, resolve, } from 'path';
8
8
  import { fileURLToPath, pathToFileURL } from 'url';
9
9
  import { Command, CommanderError } from 'commander';
10
10
  import pc from 'picocolors';
11
11
  import { logger } from './src/logger.js';
12
12
  import { determinePackageManager, parsePackageManager, runInit, } from './src/init.js';
13
- import { SCREENCI_DISABLE_RECORDING_TIMINGS_ENV, SCREENCI_MOCK_RECORD_ENV, } from './src/runtimeMode.js';
13
+ import { SCREENCI_DISABLE_RECORDING_TIMINGS_ENV, SCREENCI_MOCK_RECORD_ENV, SCREENCI_LANGUAGES_ENV, SCREENCI_TEXT_OVERRIDES_ENV, SCREENCI_RECORD_OPTIONS_ENV, } from './src/runtimeMode.js';
14
14
  import { DEFAULT_RECORD_UPLOAD_POLICY } from './src/defaults.js';
15
15
  import { findDuplicateTitles, formatDuplicateTitlesMessage, } from './src/titleValidation.js';
16
16
  import { createLinkSessionSpec, deletePersistedLinkSessionSpec, getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl, getLinkSessionFilePath, getScreenCIEnvironment, isStoredLinkSessionReusable, readPersistedLinkSessionSpec, SCREENCI_LINK_SESSION_FILE, writePersistedLinkSessionSpec, } from './src/linkSession.js';
@@ -27,11 +27,15 @@ const SCREENCI_LINK_SESSION_POLL_INTERVAL_MS = 2_000;
27
27
  // sign-in. We poll on a slower cadence than the interactive loop so a long wait
28
28
  // for a human to click the link does not hammer the backend.
29
29
  const SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS = 5_000;
30
- // `record --poll-auth` does not wait forever: after this long without a
31
- // completed sign-in we stop polling and exit cleanly so an agent or CI step does
32
- // not hang indefinitely. The link stays valid, so the command can be rerun. The
33
- // default can be overridden with SCREENCI_POLL_AUTH_TIMEOUT_MS (milliseconds).
30
+ // A waiting `record` does not wait forever: after this long without a completed
31
+ // sign-in we stop polling so a non-interactive agent run does not hang. The link
32
+ // stays valid, so the command can be rerun. The default can be overridden with
33
+ // SCREENCI_POLL_AUTH_TIMEOUT_MS (milliseconds).
34
34
  const SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS = 5 * 60 * 1_000;
35
+ // An interactive `record` (a human at a terminal) waits longer before giving up,
36
+ // since a person may take a while to open the link and finish signing in. Also
37
+ // overridable with SCREENCI_POLL_AUTH_TIMEOUT_MS.
38
+ const SCREENCI_LINK_SESSION_INTERACTIVE_TIMEOUT_MS = 15 * 60 * 1_000;
35
39
  const require = createRequire(import.meta.url);
36
40
  /**
37
41
  * Reports whether the current session can complete an interactive browser
@@ -184,7 +188,7 @@ function resolveRecordingFileCandidates(filePath, configDir, sourceFilePath) {
184
188
  return [
185
189
  filePath,
186
190
  ...(sourceFileCandidate ? [sourceFileCandidate] : []),
187
- resolve(configDir, 'videos', filePath),
191
+ resolve(configDir, 'recordings', filePath),
188
192
  resolve(configDir, pathRelative('/app', filePath)),
189
193
  ];
190
194
  }
@@ -243,6 +247,17 @@ class RecordFailureHintError extends Error {
243
247
  this.cause = cause;
244
248
  }
245
249
  }
250
+ // Thrown when we wait for a browser sign-in and the deadline passes before it
251
+ // completes. The link stays valid, so the message tells the user to sign in and
252
+ // rerun. A timed-out sign-in means recording did not happen, so the caller maps
253
+ // this to a non-zero exit (it is a failure, not the clean print-and-exit handoff
254
+ // used by CI).
255
+ export class SignInTimeoutError extends Error {
256
+ constructor(message) {
257
+ super(message);
258
+ this.name = 'SignInTimeoutError';
259
+ }
260
+ }
246
261
  function isUploadCancelledError(err) {
247
262
  return (err instanceof UploadCancelledError ||
248
263
  (err instanceof Error &&
@@ -1004,7 +1019,7 @@ export function formatFailedVideoMessage(videoName, message) {
1004
1019
  if (message.startsWith(EXPRESSIVE_TIER_ERROR_PREFIX)) {
1005
1020
  return [
1006
1021
  `${videoName}: ${message}`,
1007
- "If you want to keep using the current tier, remove `voice.style` or `modelType: 'expressive'` from `createNarration()`.",
1022
+ "If you want to keep using the current tier, remove `voice.style` or `modelType: 'expressive'` from the localize `voice`.",
1008
1023
  ].join('\n');
1009
1024
  }
1010
1025
  return `${videoName}: ${message}`;
@@ -1017,6 +1032,13 @@ export function printUploadStartFailureMessage(videoName, status, responseText,
1017
1032
  }
1018
1033
  logger.warn(message);
1019
1034
  }
1035
+ export function displayAssetPath(assetPath) {
1036
+ if (!isAbsolute(assetPath)) {
1037
+ return assetPath;
1038
+ }
1039
+ const rel = pathRelative(process.cwd(), assetPath);
1040
+ return rel.length > 0 ? rel : assetPath;
1041
+ }
1020
1042
  async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIfAborted, progressReporter) {
1021
1043
  const logInfo = (message) => {
1022
1044
  if (progressReporter) {
@@ -1050,7 +1072,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
1050
1072
  }
1051
1073
  const checkBody = (await checkRes.json());
1052
1074
  if (checkBody.exists) {
1053
- logInfo(`${pc.green('✔')} Asset already exists: ${asset.path}`);
1075
+ logInfo(`${pc.green('✔')} Asset already exists: ${displayAssetPath(asset.path)}`);
1054
1076
  continue;
1055
1077
  }
1056
1078
  if (!asset.fileBuffer || !asset.contentType) {
@@ -1077,14 +1099,14 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
1077
1099
  if (!res.ok) {
1078
1100
  const text = await res.text();
1079
1101
  if (res.status === 409 && text.includes('already exists')) {
1080
- logInfo(`${pc.green('✔')} Asset already exists: ${asset.path}`);
1102
+ logInfo(`${pc.green('✔')} Asset already exists: ${displayAssetPath(asset.path)}`);
1081
1103
  }
1082
1104
  else {
1083
1105
  throw new UploadAssetError(`Failed to upload asset ${asset.path}: ${res.status} ${text}${hint401(res.status, secret)}`);
1084
1106
  }
1085
1107
  }
1086
1108
  else {
1087
- logInfo(`Asset uploaded: ${asset.path}`);
1109
+ logInfo(`Asset uploaded: ${displayAssetPath(asset.path)}`);
1088
1110
  }
1089
1111
  }
1090
1112
  catch (err) {
@@ -1387,8 +1409,22 @@ async function loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath) {
1387
1409
  }
1388
1410
  async function requireScreenCISecret(configPath, opts = {}) {
1389
1411
  const { resolvedConfigPath, screenciConfig } = await loadScreenCIConfigAndEnv(configPath);
1390
- const secret = process.env.SCREENCI_SECRET ??
1391
- (await ensureScreenciSecret(resolvedConfigPath, opts));
1412
+ let secret;
1413
+ try {
1414
+ secret =
1415
+ process.env.SCREENCI_SECRET ??
1416
+ (await ensureScreenciSecret(resolvedConfigPath, opts));
1417
+ }
1418
+ catch (err) {
1419
+ // A waited-out sign-in means recording did not happen: print the (already
1420
+ // formatted) message and exit non-zero so the failure is loud, instead of
1421
+ // looking like a successful run.
1422
+ if (err instanceof SignInTimeoutError) {
1423
+ logger.error(err.message);
1424
+ process.exit(1);
1425
+ }
1426
+ throw err;
1427
+ }
1392
1428
  if (!secret) {
1393
1429
  // In a non-interactive session ensureScreenciSecret already printed the
1394
1430
  // sign-in link and the next step, so we exit without repeating guidance.
@@ -1447,7 +1483,7 @@ export function extractGrep(args) {
1447
1483
  // authenticated commands; the backend dispatches the workflow using the GitHub
1448
1484
  // token stored for the project. An optional `--grep` records only matching
1449
1485
  // videos/screenshots.
1450
- async function triggerRemoteRun(configPath, grep) {
1486
+ async function triggerRemoteRun(configPath, grep, languages) {
1451
1487
  const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath, { interactive: detectInteractiveSession() });
1452
1488
  const res = await fetch(`${apiUrl}/cli/trigger-run`, {
1453
1489
  method: 'POST',
@@ -1458,14 +1494,19 @@ async function triggerRemoteRun(configPath, grep) {
1458
1494
  body: JSON.stringify({
1459
1495
  projectName: screenciConfig.projectName,
1460
1496
  ...(grep ? { grep } : {}),
1497
+ ...(languages ? { languages } : {}),
1461
1498
  }),
1462
1499
  });
1463
1500
  if (!res.ok) {
1464
1501
  const text = await res.text();
1465
1502
  throw new Error(`Failed to trigger remote run: ${res.status} ${text}${hint401(res.status, secret)}`);
1466
1503
  }
1467
- logger.info(grep
1468
- ? `Triggered the remote recording workflow for "${screenciConfig.projectName}" (filter: ${grep}).`
1504
+ const filters = [
1505
+ ...(grep ? [`filter: ${grep}`] : []),
1506
+ ...(languages ? [`languages: ${languages}`] : []),
1507
+ ];
1508
+ logger.info(filters.length > 0
1509
+ ? `Triggered the remote recording workflow for "${screenciConfig.projectName}" (${filters.join(', ')}).`
1469
1510
  : `Triggered the remote recording workflow for "${screenciConfig.projectName}".`);
1470
1511
  }
1471
1512
  function getLastRecordFilePath(screenciDir) {
@@ -1562,14 +1603,12 @@ async function pollLinkSessionOnce(spec) {
1562
1603
  }
1563
1604
  return { status };
1564
1605
  }
1565
- function getPollAuthTimeoutMs() {
1606
+ function getAuthPollTimeoutMs(defaultMs) {
1566
1607
  const raw = process.env.SCREENCI_POLL_AUTH_TIMEOUT_MS;
1567
1608
  if (raw === undefined)
1568
- return SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS;
1609
+ return defaultMs;
1569
1610
  const parsed = Number(raw);
1570
- return Number.isFinite(parsed) && parsed >= 0
1571
- ? parsed
1572
- : SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS;
1611
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : defaultMs;
1573
1612
  }
1574
1613
  async function pollLinkSession(spec, pollIntervalMs = SCREENCI_LINK_SESSION_POLL_INTERVAL_MS, deadlineEpochMs) {
1575
1614
  for (;;) {
@@ -1593,6 +1632,7 @@ async function pollLinkSession(spec, pollIntervalMs = SCREENCI_LINK_SESSION_POLL
1593
1632
  export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
1594
1633
  const interactive = opts.interactive ?? true;
1595
1634
  const pollAuth = opts.pollAuth ?? false;
1635
+ const noPollAuth = opts.noPollAuth ?? false;
1596
1636
  const existingSecret = process.env.SCREENCI_SECRET;
1597
1637
  if (existingSecret)
1598
1638
  return existingSecret;
@@ -1634,69 +1674,80 @@ export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
1634
1674
  logger.info(`Successfully saved SCREENCI_SECRET to ${envFilePath}`);
1635
1675
  return secret;
1636
1676
  };
1637
- if (!interactive) {
1638
- // Non-interactive sessions cannot complete a browser sign-in here, so by
1639
- // default we never block. Reuse or create the persisted session and check
1640
- // its status once; if a stored session is stale, recreate it and check
1641
- // once more. When the session is already completed (the sign-in happened
1642
- // between runs) we pick up the secret; otherwise we print the link and
1643
- // return so the caller can surface it and rerun later. The exception is
1644
- // `pollAuth` (the `--poll-auth` flag), which opts in to waiting: it keeps
1645
- // polling on a slow cadence until sign-in completes.
1646
- let spec = await ensureSpec();
1647
- let result = await pollLinkSessionOnce(spec);
1648
- if (result.status === 'expired' ||
1649
- result.status === 'consumed' ||
1650
- result.status === 'invalid') {
1651
- deletePersistedLinkSessionSpec(specPath);
1652
- spec = await ensureSpec();
1653
- result = await pollLinkSessionOnce(spec);
1654
- }
1655
- if (result.status === 'completed' && result.secret) {
1656
- return await saveCompletedSecret(result.secret);
1657
- }
1658
- if (pollAuth) {
1659
- // The caller asked us to wait for sign-in. Print the link, then poll on
1660
- // a slow cadence until the human finishes signing in, and continue
1661
- // recording automatically once the secret lands. We re-print the link
1662
- // on every (re)created session so the latest valid link is always
1663
- // visible, including after a stale session is recreated. We do not wait
1664
- // forever: after a default timeout we stop polling and exit cleanly so
1665
- // an agent or CI step does not hang. The link stays valid for a rerun.
1666
- const timeoutMs = getPollAuthTimeoutMs();
1667
- const timeoutMinutes = Math.round(timeoutMs / 60_000);
1668
- const deadlineEpochMs = Date.now() + timeoutMs;
1669
- for (;;) {
1670
- logger.info(`Sign-in required to record. Open this link to sign in:\n${pc.cyan(spec.appUrl)}\n` +
1671
- `Waiting for sign-in (checking every 5 seconds, up to ${timeoutMinutes} minutes). Recording continues automatically once you finish.`);
1672
- const polled = await pollLinkSession(spec, SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS, deadlineEpochMs);
1673
- if (polled.status === 'completed' && polled.secret) {
1674
- return await saveCompletedSecret(polled.secret);
1675
- }
1676
- if (polled.status === 'timed-out' || Date.now() >= deadlineEpochMs) {
1677
- logger.info(`Timed out after ${timeoutMinutes} minutes waiting for sign-in. The link is still valid:\n${pc.cyan(spec.appUrl)}\n` +
1678
- `After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to continue, or ${pc.cyan(getSuggestedScreenciCommand('record', '--poll-auth'))} to wait again.`);
1679
- return undefined;
1680
- }
1681
- deletePersistedLinkSessionSpec(specPath);
1682
- spec = await ensureSpec();
1683
- }
1684
- }
1677
+ // Decide whether this run waits for a browser sign-in or just prints the
1678
+ // link and hands off. A human at a terminal always waits. A plain
1679
+ // non-interactive run (a coding agent: no terminal, no CI) also waits by
1680
+ // default, so `npx screenci record` records without needing a flag. CI and
1681
+ // an explicit SCREENCI_NONINTERACTIVE never wait (CI must not hang and is
1682
+ // expected to provide SCREENCI_SECRET), and `--no-poll-auth` opts back into
1683
+ // that print-and-exit handoff. `--poll-auth` forces waiting and is kept for
1684
+ // backwards compatibility (it is the default for agents now).
1685
+ const isCI = process.env.CI === 'true';
1686
+ const nonInteractiveForced = process.env.SCREENCI_NONINTERACTIVE === '1';
1687
+ const waitForSignIn = noPollAuth
1688
+ ? false
1689
+ : interactive || pollAuth || (!isCI && !nonInteractiveForced);
1690
+ const pollIntervalMs = interactive
1691
+ ? SCREENCI_LINK_SESSION_POLL_INTERVAL_MS
1692
+ : SCREENCI_LINK_SESSION_POLL_FLAG_INTERVAL_MS;
1693
+ const timeoutMs = getAuthPollTimeoutMs(interactive
1694
+ ? SCREENCI_LINK_SESSION_INTERACTIVE_TIMEOUT_MS
1695
+ : SCREENCI_LINK_SESSION_POLL_FLAG_TIMEOUT_MS);
1696
+ // Check the current/stored session once. If sign-in already completed (for
1697
+ // example between runs) we pick up the secret immediately. Recreate a stale
1698
+ // session and check once more before deciding to wait or hand off.
1699
+ let spec = await ensureSpec();
1700
+ let result = await pollLinkSessionOnce(spec);
1701
+ if (result.status === 'expired' ||
1702
+ result.status === 'consumed' ||
1703
+ result.status === 'invalid') {
1704
+ deletePersistedLinkSessionSpec(specPath);
1705
+ spec = await ensureSpec();
1706
+ result = await pollLinkSessionOnce(spec);
1707
+ }
1708
+ if (result.status === 'completed' && result.secret) {
1709
+ return await saveCompletedSecret(result.secret);
1710
+ }
1711
+ if (!waitForSignIn) {
1712
+ // CI / SCREENCI_NONINTERACTIVE / --no-poll-auth: print the link and hand
1713
+ // off without waiting. The caller exits cleanly (0); a pending sign-in
1714
+ // here is an expected handoff, not a failure.
1685
1715
  logger.info(`Sign-in required to record. Open this link to sign in:\n${pc.cyan(spec.appUrl)}\n` +
1686
- `This session is non-interactive, so sign-in can't complete here. After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to continue, or run ${pc.cyan(getSuggestedScreenciCommand('record', '--poll-auth'))} to print the link again and wait for sign-in, continuing automatically.`);
1716
+ `This session won't wait for sign-in. After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to record. In CI, set SCREENCI_SECRET (from ${getScreenCISecretsUrl()}) to record without an interactive sign-in.`);
1687
1717
  return undefined;
1688
1718
  }
1719
+ // Wait for sign-in: print the link, then poll until the human finishes and
1720
+ // continue recording automatically once the secret lands. We re-print the
1721
+ // link on every (re)created session so the latest valid link is always
1722
+ // visible. We do not wait forever: after the timeout we throw so the caller
1723
+ // exits non-zero (a timed-out sign-in means nothing was recorded). The link
1724
+ // stays valid for a rerun.
1725
+ const timeoutMinutes = Math.max(1, Math.round(timeoutMs / 60_000));
1726
+ const intervalSeconds = Math.max(1, Math.round(pollIntervalMs / 1_000));
1727
+ const deadlineEpochMs = Date.now() + timeoutMs;
1689
1728
  for (;;) {
1690
- const spec = await ensureSpec();
1691
- logger.info(`Open this link to sign in and connect the CLI:\n${pc.cyan(spec.appUrl)}\n`);
1692
- const result = await pollLinkSession(spec);
1693
- if (result.status === 'completed' && result.secret) {
1694
- return await saveCompletedSecret(result.secret);
1729
+ logger.info(`Sign-in required to record. Open this link to sign in:\n${pc.cyan(spec.appUrl)}\n` +
1730
+ `Waiting for sign-in (checking every ${intervalSeconds} seconds, up to ${timeoutMinutes} minutes). Recording continues automatically once you finish.`);
1731
+ const polled = await pollLinkSession(spec, pollIntervalMs, deadlineEpochMs);
1732
+ if (polled.status === 'completed' && polled.secret) {
1733
+ return await saveCompletedSecret(polled.secret);
1695
1734
  }
1735
+ if (polled.status === 'timed-out' || Date.now() >= deadlineEpochMs) {
1736
+ throw new SignInTimeoutError(`Timed out after ${timeoutMinutes} minutes waiting for sign-in. The link is still valid:\n${pc.cyan(spec.appUrl)}\n` +
1737
+ `After signing in, rerun ${pc.cyan(getSuggestedScreenciCommand('record'))} to record.`);
1738
+ }
1739
+ // Session went stale (expired/consumed/invalid) before sign-in: recreate
1740
+ // and keep waiting until the deadline.
1696
1741
  deletePersistedLinkSessionSpec(specPath);
1742
+ spec = await ensureSpec();
1697
1743
  }
1698
1744
  }
1699
1745
  catch (err) {
1746
+ // A timed-out sign-in is a deliberate, already-formatted failure: let it
1747
+ // propagate so the caller can exit non-zero rather than swallowing it as a
1748
+ // generic auth error.
1749
+ if (err instanceof SignInTimeoutError)
1750
+ throw err;
1700
1751
  const msg = err instanceof Error ? err.message : String(err);
1701
1752
  logger.warn(`Authentication failed: ${msg}`);
1702
1753
  logger.info(`You can add SCREENCI_SECRET manually to ${resolvedConfigPath ? await resolveProjectEnvFilePath(resolvedConfigPath) : '.env'} later. Get it from ${getScreenCISecretsUrl()}.`);
@@ -1719,21 +1770,24 @@ export async function main() {
1719
1770
  .command('record [playwrightArgs...]')
1720
1771
  .description('Record videos using Playwright')
1721
1772
  .option('-v, --verbose', 'verbose output')
1722
- .option('--poll-auth', 'wait for sign-in to complete (polling every 5s, up to 5 minutes) instead of exiting, then continue recording')
1773
+ .option('--poll-auth', 'wait for sign-in before recording (now the default off-CI; kept for backwards compatibility)')
1774
+ .option('--no-poll-auth', 'do not wait for sign-in: print the link and exit cleanly if not signed in (the default under CI)')
1723
1775
  .option('--remote', 'trigger the GitHub Actions recording workflow for this project remotely instead of recording locally')
1776
+ .option('--languages <langs>', 'record/render only these languages (comma-separated, e.g. fi,en)')
1724
1777
  .allowUnknownOption(true)
1725
1778
  .action(async () => {
1726
1779
  const parsed = parseRecordCliArgs(getSubcommandArgv('record'));
1727
1780
  // `--remote` is a pure dispatch: it fires the project's GitHub Actions
1728
1781
  // recording workflow and exits, so there is no local Playwright run. A
1729
- // pass-through `--grep` becomes the remote recording filter.
1782
+ // pass-through `--grep` becomes the remote recording filter, and
1783
+ // `--languages` limits which language versions are recorded.
1730
1784
  if (parsed.remote) {
1731
- await triggerRemoteRun(parsed.configPath, extractGrep(parsed.otherArgs));
1785
+ await triggerRemoteRun(parsed.configPath, extractGrep(parsed.otherArgs), parsed.languages);
1732
1786
  return;
1733
1787
  }
1734
1788
  let playwrightFailure = null;
1735
1789
  try {
1736
- await run('record', parsed.otherArgs, parsed.configPath, parsed.verbose, false, parsed.pollAuth);
1790
+ await run('record', parsed.otherArgs, parsed.configPath, parsed.verbose, false, parsed.pollAuth, parsed.languages, parsed.noPollAuth);
1737
1791
  }
1738
1792
  catch (error) {
1739
1793
  if (!(error instanceof Error))
@@ -1966,11 +2020,13 @@ function getSubcommandArgv(command) {
1966
2020
  const commandIndex = argv.indexOf(command);
1967
2021
  return commandIndex === -1 ? [] : argv.slice(commandIndex + 1);
1968
2022
  }
1969
- function parseRecordCliArgs(args) {
2023
+ export function parseRecordCliArgs(args) {
1970
2024
  let configPath;
1971
2025
  let verbose = false;
1972
2026
  let pollAuth = false;
2027
+ let noPollAuth = false;
1973
2028
  let remote = false;
2029
+ let languages;
1974
2030
  const otherArgs = [];
1975
2031
  for (let i = 0; i < args.length; i++) {
1976
2032
  const arg = args[i];
@@ -1985,12 +2041,29 @@ function parseRecordCliArgs(args) {
1985
2041
  configPath = nextArg;
1986
2042
  i++;
1987
2043
  }
2044
+ else if (arg === '--languages' || arg === '--language') {
2045
+ // screenci-only flag: parsed out so it is not forwarded to Playwright.
2046
+ const nextArg = args[i + 1];
2047
+ if (nextArg === undefined) {
2048
+ logger.error('Error: --languages requires a comma-separated value');
2049
+ process.exit(1);
2050
+ }
2051
+ languages = nextArg;
2052
+ i++;
2053
+ }
2054
+ else if (arg.startsWith('--languages=') ||
2055
+ arg.startsWith('--language=')) {
2056
+ languages = arg.slice(arg.indexOf('=') + 1);
2057
+ }
1988
2058
  else if (arg === '--verbose' || arg === '-v') {
1989
2059
  verbose = true;
1990
2060
  }
1991
2061
  else if (arg === '--poll-auth') {
1992
2062
  pollAuth = true;
1993
2063
  }
2064
+ else if (arg === '--no-poll-auth') {
2065
+ noPollAuth = true;
2066
+ }
1994
2067
  else if (arg === '--remote') {
1995
2068
  remote = true;
1996
2069
  }
@@ -2002,7 +2075,9 @@ function parseRecordCliArgs(args) {
2002
2075
  configPath,
2003
2076
  verbose,
2004
2077
  pollAuth,
2078
+ noPollAuth,
2005
2079
  remote,
2080
+ languages,
2006
2081
  otherArgs,
2007
2082
  };
2008
2083
  }
@@ -2053,7 +2128,85 @@ function validateArgs(args) {
2053
2128
  }
2054
2129
  }
2055
2130
  }
2056
- async function run(command, additionalArgs, customConfigPath, verbose = false, mockRecord = false, pollAuth = false) {
2131
+ /**
2132
+ * Fetch the project's current Studio text-field overrides so they can be
2133
+ * injected as SCREENCI_TEXT_OVERRIDES before a recording. On-screen `text`
2134
+ * fields render during the recording (they cannot be patched at render time),
2135
+ * so any Studio edits must be present here; code-declared seeds are the
2136
+ * fallback. Best-effort: any failure returns an empty env so the SDK uses the
2137
+ * code seeds, and a recording is never blocked by this fetch.
2138
+ */
2139
+ async function fetchTextOverridesEnv(configPath, languages, verbose) {
2140
+ try {
2141
+ const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath, { interactive: false });
2142
+ const params = new URLSearchParams({
2143
+ projectName: screenciConfig.projectName,
2144
+ });
2145
+ if (languages)
2146
+ params.set('languages', languages);
2147
+ const res = await fetch(`${apiUrl}/cli/text-overrides?${params.toString()}`, { headers: { 'X-ScreenCI-Secret': secret } });
2148
+ if (!res.ok) {
2149
+ if (verbose) {
2150
+ logger.warn(`Could not fetch Studio text overrides (${res.status}); using code-declared values.`);
2151
+ }
2152
+ return {};
2153
+ }
2154
+ const body = (await res.json());
2155
+ const overrides = body.overrides;
2156
+ if (overrides === undefined ||
2157
+ overrides === null ||
2158
+ typeof overrides !== 'object' ||
2159
+ Object.keys(overrides).length === 0) {
2160
+ return {};
2161
+ }
2162
+ return { [SCREENCI_TEXT_OVERRIDES_ENV]: JSON.stringify(overrides) };
2163
+ }
2164
+ catch (error) {
2165
+ if (verbose) {
2166
+ logger.warn(`Could not fetch Studio text overrides; using code-declared values.${error instanceof Error ? ` (${error.message})` : ''}`);
2167
+ }
2168
+ return {};
2169
+ }
2170
+ }
2171
+ /**
2172
+ * Fetch the project's current Studio record-option overrides so they can be
2173
+ * injected as SCREENCI_RECORD_OPTIONS before a recording. Record options (aspect
2174
+ * ratio, quality, fps) change the captured viewport/encode, so any Studio edits
2175
+ * must be present here; code-declared values are the fallback. Best-effort: any
2176
+ * failure returns an empty env so the SDK uses the code values, and a recording
2177
+ * is never blocked by this fetch.
2178
+ */
2179
+ async function fetchRecordOptionsEnv(configPath, verbose) {
2180
+ try {
2181
+ const { screenciConfig, secret, apiUrl } = await requireScreenCISecret(configPath, { interactive: false });
2182
+ const params = new URLSearchParams({
2183
+ projectName: screenciConfig.projectName,
2184
+ });
2185
+ const res = await fetch(`${apiUrl}/cli/record-options?${params.toString()}`, { headers: { 'X-ScreenCI-Secret': secret } });
2186
+ if (!res.ok) {
2187
+ if (verbose) {
2188
+ logger.warn(`Could not fetch Studio record options (${res.status}); using code-declared values.`);
2189
+ }
2190
+ return {};
2191
+ }
2192
+ const body = (await res.json());
2193
+ const recordOptions = body.recordOptions;
2194
+ if (recordOptions === undefined ||
2195
+ recordOptions === null ||
2196
+ typeof recordOptions !== 'object' ||
2197
+ Object.keys(recordOptions).length === 0) {
2198
+ return {};
2199
+ }
2200
+ return { [SCREENCI_RECORD_OPTIONS_ENV]: JSON.stringify(recordOptions) };
2201
+ }
2202
+ catch (error) {
2203
+ if (verbose) {
2204
+ logger.warn(`Could not fetch Studio record options; using code-declared values.${error instanceof Error ? ` (${error.message})` : ''}`);
2205
+ }
2206
+ return {};
2207
+ }
2208
+ }
2209
+ async function run(command, additionalArgs, customConfigPath, verbose = false, mockRecord = false, pollAuth = false, languages, noPollAuth = false) {
2057
2210
  const configPath = resolveScreenCIConfigPathOrExit(customConfigPath);
2058
2211
  if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
2059
2212
  await loadEnvFileFromConfigSource(configPath, false);
@@ -2064,17 +2217,32 @@ async function run(command, additionalArgs, customConfigPath, verbose = false, m
2064
2217
  await requireScreenCISecret(configPath, {
2065
2218
  interactive: detectInteractiveSession(),
2066
2219
  pollAuth,
2220
+ noPollAuth,
2067
2221
  });
2068
2222
  }
2069
2223
  validateArgs(additionalArgs);
2070
2224
  const screenciDir = resolve(dirname(configPath), '.screenci');
2071
2225
  clearRecordingDirectories(screenciDir);
2072
2226
  }
2227
+ // Studio text-field overrides are injected for the record command only: they
2228
+ // resolve in the SDK's `text` fixture before the recording runs. `test` (the
2229
+ // preview run) keeps code-declared values.
2230
+ const textOverridesEnv = command === 'record'
2231
+ ? await fetchTextOverridesEnv(configPath, languages, verbose)
2232
+ : {};
2233
+ // Studio record-option overrides are likewise injected for record only: they
2234
+ // resolve in the SDK's `recordOptions` fixture before the capture runs.
2235
+ const recordOptionsEnv = command === 'record' ? await fetchRecordOptionsEnv(configPath, verbose) : {};
2073
2236
  const envForChild = { ...process.env };
2074
2237
  await validateUniqueDiscoveredTestTitles(configPath, additionalArgs, {
2075
2238
  ...envForChild,
2076
2239
  SCREENCI_CONFIG_DIR: dirname(configPath),
2077
2240
  ...(command === 'record' ? { SCREENCI_RECORDING: 'true' } : {}),
2241
+ ...(command === 'record' && languages
2242
+ ? { [SCREENCI_LANGUAGES_ENV]: languages }
2243
+ : {}),
2244
+ ...textOverridesEnv,
2245
+ ...recordOptionsEnv,
2078
2246
  ...(command === 'test' && !mockRecord
2079
2247
  ? { [SCREENCI_DISABLE_RECORDING_TIMINGS_ENV]: 'true' }
2080
2248
  : {}),
@@ -2101,6 +2269,14 @@ async function run(command, additionalArgs, customConfigPath, verbose = false, m
2101
2269
  SCREENCI_CONFIG_DIR: dirname(configPath),
2102
2270
  // Enable recording only for record command
2103
2271
  ...(command === 'record' ? { SCREENCI_RECORDING: 'true' } : {}),
2272
+ // Per-language filter: the builder records only these languages.
2273
+ ...(command === 'record' && languages
2274
+ ? { [SCREENCI_LANGUAGES_ENV]: languages }
2275
+ : {}),
2276
+ // Studio text-field overrides resolved from the backend (record only).
2277
+ ...textOverridesEnv,
2278
+ // Studio record-option overrides resolved from the backend (record only).
2279
+ ...recordOptionsEnv,
2104
2280
  ...(command === 'test' && !mockRecord
2105
2281
  ? { [SCREENCI_DISABLE_RECORDING_TIMINGS_ENV]: 'true' }
2106
2282
  : {}),