screenci 0.0.8 → 0.0.10

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.
@@ -0,0 +1,34 @@
1
+ # Recording runtime ───────────────────────────────────────────────────────────
2
+ FROM docker.io/library/node:25.2.1-slim
3
+
4
+ WORKDIR /app
5
+
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ xvfb \
8
+ ffmpeg \
9
+ x11-utils \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # ── Dependency layer (cached until package.json changes) ──────────────────────
13
+ # Install screenci as a workspace package so npm creates the bin link.
14
+ COPY package.json ./screenci/
15
+ RUN printf '{"private":true,"workspaces":["screenci"]}' > package.json && npm install
16
+
17
+ # Playwright browser download: only re-runs when the playwright version changes.
18
+ RUN npx playwright install chromium --with-deps
19
+
20
+ # ── screenci build output ─────────────────────────────────────────────────────
21
+ # Copy pre-built dist directly from the screenci package directory.
22
+ COPY dist ./screenci/dist/
23
+
24
+ # Explicit bin wrapper — no npm bin-linking magic needed.
25
+ RUN printf '#!/bin/sh\nexec node /app/screenci/dist/cli.js "$@"\n' > /app/node_modules/.bin/screenci && \
26
+ chmod +x /app/node_modules/.bin/screenci
27
+
28
+ # Create .screenci directory for recordings
29
+ RUN mkdir -p .screenci
30
+
31
+ # Add node_modules/.bin to PATH
32
+ ENV PATH="/app/node_modules/.bin:${PATH}"
33
+
34
+ CMD ["echo", "Container ready"]
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AAqrBA,wBAAsB,IAAI,kBAoJzB;AAqED,wBAAgB,sBAAsB,IAAI,MAAM,CAc/C"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AAgzBA,wBAAsB,IAAI,kBA2JzB;AAqED,wBAAgB,sBAAsB,IAAI,MAAM,CAc/C"}
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { spawn, spawnSync } from 'child_process';
3
3
  import { createReadStream } from 'fs';
4
- import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync } from 'fs';
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, } from 'fs';
5
5
  import { createHash } from 'crypto';
6
6
  import { createServer } from 'http';
7
7
  import { mkdir, readdir, readFile, stat, writeFile } from 'fs/promises';
@@ -9,6 +9,80 @@ import { dirname, relative as pathRelative, resolve } from 'path';
9
9
  import { createInterface } from 'readline/promises';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { logger } from './src/logger.js';
12
+ function writeInline(msg) {
13
+ process.stdout.write(msg);
14
+ }
15
+ function completeInline(msg) {
16
+ process.stdout.write(`\r\x1b[K${msg}\n`);
17
+ }
18
+ function parseDockerfileVersion(dockerfilePath) {
19
+ let content;
20
+ try {
21
+ content = readFileSync(dockerfilePath, 'utf-8');
22
+ }
23
+ catch {
24
+ return 'unknown';
25
+ }
26
+ const fromLine = content
27
+ .split('\n')
28
+ .find((line) => line.trim().toUpperCase().startsWith('FROM'));
29
+ if (!fromLine)
30
+ return 'unknown';
31
+ const match = fromLine.match(/:([^\s@]+)/);
32
+ return match?.[1] ?? 'unknown';
33
+ }
34
+ function spawnSilent(cmd, args) {
35
+ return new Promise((resolve, reject) => {
36
+ const child = spawn(cmd, args, { stdio: 'pipe' });
37
+ child.on('close', (code) => {
38
+ if (code === 0) {
39
+ resolve();
40
+ }
41
+ else {
42
+ reject(new Error(`${cmd} exited with code ${code}`));
43
+ }
44
+ });
45
+ child.on('error', reject);
46
+ });
47
+ }
48
+ const CONTAINER_LOG_FILTER = [
49
+ /^Running ScreenCI /,
50
+ /^Using config:/,
51
+ /^Starting Xvfb /,
52
+ /^Xvfb started /,
53
+ /^Recording video to:/,
54
+ /^Recording with /,
55
+ /^Stopping recording\.\.\./,
56
+ /^FFmpeg exited /,
57
+ /^Video saved to:/,
58
+ /^Events saved to:/,
59
+ ];
60
+ function spawnContainerRecording(cmd, args) {
61
+ return new Promise((resolve, reject) => {
62
+ const child = spawn(cmd, args, { stdio: ['inherit', 'pipe', 'pipe'] });
63
+ function forwardFiltered(chunk, out) {
64
+ const lines = chunk.toString().split('\n');
65
+ for (const line of lines) {
66
+ if (line === '')
67
+ continue;
68
+ if (!CONTAINER_LOG_FILTER.some((re) => re.test(line.trimStart()))) {
69
+ out.write(line + '\n');
70
+ }
71
+ }
72
+ }
73
+ child.stdout?.on('data', (chunk) => forwardFiltered(chunk, process.stdout));
74
+ child.stderr?.on('data', (chunk) => forwardFiltered(chunk, process.stderr));
75
+ child.on('close', (code) => {
76
+ if (code === 0) {
77
+ resolve();
78
+ }
79
+ else {
80
+ reject(new Error(`${cmd} exited with code ${code}`));
81
+ }
82
+ });
83
+ child.on('error', reject);
84
+ });
85
+ }
12
86
  function clearDirectory(dir) {
13
87
  mkdirSync(dir, { recursive: true });
14
88
  for (const entry of readdirSync(dir)) {
@@ -34,7 +108,9 @@ function findRepoRoot(startDir) {
34
108
  let current = startDir;
35
109
  while (true) {
36
110
  if (existsSync(resolve(current, '.git')) ||
37
- existsSync(resolve(current, 'pnpm-workspace.yaml'))) {
111
+ existsSync(resolve(current, 'pnpm-workspace.yaml')) ||
112
+ existsSync(resolve(current, 'package-lock.json')) ||
113
+ existsSync(resolve(current, 'yarn.lock'))) {
38
114
  return current;
39
115
  }
40
116
  const parent = resolve(current, '..');
@@ -52,6 +128,8 @@ function parseArgs(args) {
52
128
  }
53
129
  let configPath;
54
130
  let noContainer = false;
131
+ let imageTag;
132
+ let verbose = false;
55
133
  const otherArgs = [];
56
134
  for (let i = 1; i < args.length; i++) {
57
135
  const arg = args[i];
@@ -69,11 +147,25 @@ function parseArgs(args) {
69
147
  else if (arg === '--no-container') {
70
148
  noContainer = true;
71
149
  }
150
+ else if (arg === '--verbose' || arg === '-v') {
151
+ verbose = true;
152
+ }
153
+ else if (arg === '--tag') {
154
+ const nextArg = args[i + 1];
155
+ if (nextArg !== undefined) {
156
+ imageTag = nextArg;
157
+ i++; // skip next arg
158
+ }
159
+ else {
160
+ logger.error('Error: --tag requires a tag argument');
161
+ process.exit(1);
162
+ }
163
+ }
72
164
  else if (arg !== undefined) {
73
165
  otherArgs.push(arg);
74
166
  }
75
167
  }
76
- return { command, configPath, noContainer, otherArgs };
168
+ return { command, configPath, noContainer, imageTag, verbose, otherArgs };
77
169
  }
78
170
  async function findLatestEntry(screenciDir) {
79
171
  let entries;
@@ -185,11 +277,12 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
185
277
  }
186
278
  catch {
187
279
  logger.warn('No .screenci directory found, skipping upload');
188
- return;
280
+ return null;
189
281
  }
190
282
  if (specificEntry !== undefined) {
191
283
  entries = entries.filter((e) => e === specificEntry);
192
284
  }
285
+ let firstProjectId = null;
193
286
  for (const entry of entries) {
194
287
  const dataJsonPath = resolve(screenciDir, entry, 'data.json');
195
288
  if (!existsSync(dataJsonPath))
@@ -204,7 +297,7 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
204
297
  continue;
205
298
  }
206
299
  const videoName = data.metadata?.videoName ?? entry;
207
- logger.info(`Uploading "${videoName}"...`);
300
+ writeInline(`Uploading "${videoName}"...`);
208
301
  try {
209
302
  // Step 1: register upload and get recordingId
210
303
  const startResponse = await fetch(`${apiUrl}/cli/upload/start`, {
@@ -217,10 +310,14 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
217
310
  });
218
311
  if (!startResponse.ok) {
219
312
  const text = await startResponse.text();
313
+ process.stdout.write('\n');
220
314
  logger.warn(`Failed to start upload for "${videoName}": ${startResponse.status} ${text}`);
221
315
  continue;
222
316
  }
223
- const { recordingId } = (await startResponse.json());
317
+ const { recordingId, projectId } = (await startResponse.json());
318
+ if (firstProjectId === null) {
319
+ firstProjectId = projectId;
320
+ }
224
321
  // Step 1b: upload asset files referenced in data.json
225
322
  await uploadAssets(data, apiUrl, secret, recordingId, resolve(screenciDir, '..'));
226
323
  // Step 2: stream the recording video file (if it exists)
@@ -241,16 +338,19 @@ async function uploadRecordings(screenciDir, projectName, apiUrl, secret, specif
241
338
  });
242
339
  if (!recordingResponse.ok) {
243
340
  const text = await recordingResponse.text();
341
+ process.stdout.write('\n');
244
342
  logger.warn(`Failed to upload recording for "${videoName}": ${recordingResponse.status} ${text}`);
245
343
  continue;
246
344
  }
247
345
  }
248
- logger.info(`Uploaded "${videoName}" successfully`);
346
+ completeInline(`Uploading "${videoName}" ✓`);
249
347
  }
250
348
  catch (err) {
349
+ process.stdout.write('\n');
251
350
  logger.warn(`Network error uploading "${videoName}":`, err);
252
351
  }
253
352
  }
353
+ return firstProjectId;
254
354
  }
255
355
  async function uploadLatest(configPath) {
256
356
  const resolvedConfigPath = findScreenCIConfig(configPath);
@@ -279,11 +379,9 @@ async function uploadLatest(configPath) {
279
379
  logger.warn(`Failed to load env file ${envFilePath}:`, err);
280
380
  }
281
381
  }
282
- const convexUrl = screenciConfig.apiUrl ?? process.env.SCREENCI_URL;
283
- if (!convexUrl) {
284
- logger.error('No API URL configured. Set apiUrl in screenci.config.ts or SCREENCI_URL env var.');
285
- process.exit(1);
286
- }
382
+ const apiUrl = process.env.DEV_PORT
383
+ ? `http://localhost:${process.env.DEV_PORT}`
384
+ : 'https://api.screenci.com';
287
385
  const secret = process.env.SCREENCI_SECRET;
288
386
  if (!secret) {
289
387
  logger.error('No secret configured. Set SCREENCI_SECRET in your .env file (get it from the API Key page in the dashboard).');
@@ -296,15 +394,24 @@ async function uploadLatest(configPath) {
296
394
  logger.warn('No recordings found in .screenci directory');
297
395
  return;
298
396
  }
397
+ const appUrl = process.env.SCREENCI_APP_URL
398
+ ? process.env.SCREENCI_APP_URL
399
+ : process.env.DEV_PORT
400
+ ? `http://localhost:${process.env.DEV_PORT}`
401
+ : 'https://app.screenci.com';
299
402
  logger.info(`Uploading latest recording: "${latestEntry}"`);
300
- await uploadRecordings(screenciDir, screenciConfig.projectName, convexUrl, secret, latestEntry);
403
+ const projectId = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, secret, latestEntry);
404
+ if (projectId !== null) {
405
+ logger.info('');
406
+ logger.info('Recording finished, results available at:');
407
+ logger.info(`${appUrl}/project/${projectId}`);
408
+ }
301
409
  }
302
410
  function generateConfig(projectName) {
303
411
  return `import { defineConfig } from 'screenci'
304
412
 
305
413
  export default defineConfig({
306
414
  projectName: ${JSON.stringify(projectName)},
307
- apiUrl: process.env.SCREENCI_URL ?? 'http://localhost:8787',
308
415
  envFile: '.env',
309
416
  videoDir: './videos',
310
417
  forbidOnly: !!process.env.CI,
@@ -441,9 +548,9 @@ async function performBrowserLogin(appUrl) {
441
548
  const port = server.address().port;
442
549
  const callbackUrl = `http://localhost:${port}/callback`;
443
550
  const loginUrl = `${appUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
444
- logger.info('Opening browser for authentication...');
445
551
  logger.info(`If the browser does not open automatically, visit:`);
446
- logger.info(` ${loginUrl}`);
552
+ logger.info(loginUrl);
553
+ logger.info('');
447
554
  openBrowser(loginUrl);
448
555
  });
449
556
  const timeout = setTimeout(() => {
@@ -492,8 +599,9 @@ async function runInitAuth() {
492
599
  (devPort ? `http://localhost:${devPort}` : 'https://app.screenci.com');
493
600
  try {
494
601
  const secret = await performBrowserLogin(appUrl);
495
- await writeFile(resolve(process.cwd(), '.env'), `SCREENCI_SECRET=${secret}\n`);
496
- logger.info('API key saved to .env');
602
+ const savePath = resolve(process.cwd(), '.env');
603
+ await writeFile(savePath, `SCREENCI_SECRET=${secret}\n`);
604
+ logger.info(`Successfully saved SCREENCI_SECRET to ${savePath}`);
497
605
  }
498
606
  catch (err) {
499
607
  const msg = err instanceof Error ? err.message : String(err);
@@ -550,7 +658,7 @@ async function runInit(projectNameArg, localPackagePath) {
550
658
  }
551
659
  export async function main() {
552
660
  const args = process.argv.slice(2);
553
- const { command, configPath, noContainer, otherArgs } = parseArgs(args);
661
+ const { command, configPath, noContainer, imageTag, verbose, otherArgs } = parseArgs(args);
554
662
  switch (command) {
555
663
  case 'record': {
556
664
  const useContainer = !noContainer && process.env.SCREENCI_IN_CONTAINER !== 'true';
@@ -587,7 +695,7 @@ export async function main() {
587
695
  // Config import failed but SCREENCI_SECRET is already in env — continue
588
696
  }
589
697
  if (!process.env.SCREENCI_SECRET) {
590
- logger.info('SCREENCI_SECRET not found. Opening browser to sign in and select a plan...');
698
+ logger.info('No SCREENCI_SECRET in .env file, opening browser for authentication...');
591
699
  const devPort = process.env.DEV_PORT;
592
700
  const appUrl = process.env.SCREENCI_APP_URL ??
593
701
  (devPort
@@ -597,12 +705,12 @@ export async function main() {
597
705
  const savePath = envFilePath ?? resolve(dirname(resolvedConfigForSecret), '.env');
598
706
  await writeFile(savePath, `SCREENCI_SECRET=${secret}\n`);
599
707
  process.env.SCREENCI_SECRET = secret;
600
- logger.info('API key saved.');
708
+ logger.info(`Successfully saved SCREENCI_SECRET to ${savePath}`);
601
709
  }
602
710
  }
603
711
  }
604
712
  if (useContainer) {
605
- await runWithContainer(otherArgs, configPath);
713
+ await runWithContainer(otherArgs, configPath, imageTag, verbose);
606
714
  }
607
715
  else {
608
716
  await run(command, otherArgs, configPath);
@@ -610,7 +718,7 @@ export async function main() {
610
718
  // Upload only from the host, not from inside the container
611
719
  if (process.env.SCREENCI_IN_CONTAINER === 'true')
612
720
  break;
613
- // After recording, upload results to Convex if configured
721
+ // After recording, upload results to API if configured
614
722
  const resolvedConfigPath = findScreenCIConfig(configPath);
615
723
  if (resolvedConfigPath) {
616
724
  try {
@@ -625,11 +733,14 @@ export async function main() {
625
733
  logger.warn(`Failed to load env file ${envFilePath}:`, err);
626
734
  }
627
735
  }
628
- const convexUrl = screenciConfig.apiUrl ?? process.env.SCREENCI_URL;
629
- if (!convexUrl) {
630
- logger.info('No API URL configured, skipping upload. Set apiUrl in screenci.config.ts or SCREENCI_URL env var.');
631
- break;
632
- }
736
+ const apiUrl = process.env.DEV_PORT
737
+ ? `http://localhost:${process.env.DEV_PORT}`
738
+ : 'https://api.screenci.com';
739
+ const appUrl = process.env.SCREENCI_APP_URL
740
+ ? process.env.SCREENCI_APP_URL
741
+ : process.env.DEV_PORT
742
+ ? `http://localhost:${process.env.DEV_PORT}`
743
+ : 'https://app.screenci.com';
633
744
  const secret = process.env.SCREENCI_SECRET;
634
745
  if (!secret) {
635
746
  logger.info('No secret configured, skipping upload. Set SCREENCI_SECRET in your .env file.');
@@ -637,7 +748,12 @@ export async function main() {
637
748
  }
638
749
  const configDir = dirname(resolvedConfigPath);
639
750
  const screenciDir = resolve(configDir, '.screenci');
640
- await uploadRecordings(screenciDir, screenciConfig.projectName, convexUrl, secret);
751
+ const projectId = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, secret);
752
+ if (projectId !== null) {
753
+ logger.info('');
754
+ logger.info('Recording finished, results available at:');
755
+ logger.info(`${appUrl}/project/${projectId}`);
756
+ }
641
757
  }
642
758
  catch (err) {
643
759
  logger.warn('Failed to load config for upload:', err);
@@ -744,7 +860,25 @@ export function detectContainerRuntime() {
744
860
  logger.error(' docker: https://docs.docker.com/get-docker/');
745
861
  process.exit(1);
746
862
  }
747
- async function runWithContainer(additionalArgs, customConfigPath) {
863
+ async function buildImage(cmd, args, label, verbose) {
864
+ if (verbose) {
865
+ await spawnInherited(cmd, args);
866
+ return;
867
+ }
868
+ writeInline(`${label}...`);
869
+ try {
870
+ await spawnSilent(cmd, args);
871
+ completeInline(`${label} ✓`);
872
+ }
873
+ catch (err) {
874
+ process.stdout.write('\n');
875
+ const msg = err instanceof Error ? err.message : String(err);
876
+ logger.error(msg);
877
+ logger.error('Run again with --verbose to see the full build output');
878
+ process.exit(1);
879
+ }
880
+ }
881
+ async function runWithContainer(additionalArgs, customConfigPath, imageTag, verbose = false) {
748
882
  const configPath = findScreenCIConfig(customConfigPath);
749
883
  if (!configPath) {
750
884
  const errorMsg = customConfigPath
@@ -766,43 +900,92 @@ async function runWithContainer(additionalArgs, customConfigPath) {
766
900
  process.exit(1);
767
901
  }
768
902
  const containerRuntime = detectContainerRuntime();
903
+ const ghcrImage = 'ghcr.io/screenci/record:latest';
904
+ const dockerfileVersion = parseDockerfileVersion(dockerfilePath);
769
905
  if (process.env['SCREENCI_LOCAL_IMAGE']) {
770
906
  logger.info('SCREENCI_LOCAL_IMAGE set — skipping screenci image build');
771
907
  }
908
+ else if (imageTag !== undefined) {
909
+ const remoteImage = `ghcr.io/screenci/record:${imageTag}`;
910
+ const imageExists = spawnSync(containerRuntime, ['image', 'exists', remoteImage], {
911
+ stdio: 'ignore',
912
+ }).status === 0;
913
+ logger.info(`Using image tag ${imageTag} instead of the version ${dockerfileVersion} from Dockerfile`);
914
+ if (!imageExists) {
915
+ await buildImage(containerRuntime, ['pull', remoteImage], 'Pulling image', verbose);
916
+ }
917
+ await spawnSilent(containerRuntime, ['tag', remoteImage, ghcrImage]);
918
+ }
772
919
  else {
773
920
  const cliDir = dirname(fileURLToPath(import.meta.url));
921
+ const screenciPackageRoot = resolve(cliDir, '..');
774
922
  const screenciDockerfilePath = resolve(cliDir, 'Dockerfile');
775
- logger.info(`Building container image with ${containerRuntime}...`);
776
- logger.info(`Using Dockerfile: ${screenciDockerfilePath}`);
777
- logger.info(`Build context: ${repoRoot}`);
778
- await spawnInherited(containerRuntime, [
779
- 'build',
780
- '-f',
781
- screenciDockerfilePath,
782
- '-t',
783
- 'screenci',
784
- repoRoot,
785
- ]);
786
- }
787
- logger.info(`Using Dockerfile: ${dockerfilePath}`);
788
- logger.info(`Build context: ${configDir}`);
789
- await spawnInherited(containerRuntime, [
790
- 'build',
791
- '-f',
792
- dockerfilePath,
793
- '-t',
794
- 'screenci',
795
- configDir,
796
- ]);
923
+ if (verbose) {
924
+ await spawnInherited(containerRuntime, [
925
+ 'build',
926
+ '-f',
927
+ screenciDockerfilePath,
928
+ '-t',
929
+ ghcrImage,
930
+ screenciPackageRoot,
931
+ ]);
932
+ await spawnInherited(containerRuntime, [
933
+ 'build',
934
+ '-f',
935
+ dockerfilePath,
936
+ '-t',
937
+ 'screenci',
938
+ configDir,
939
+ ]);
940
+ }
941
+ else {
942
+ writeInline('Building image...');
943
+ try {
944
+ await spawnSilent(containerRuntime, [
945
+ 'build',
946
+ '-f',
947
+ screenciDockerfilePath,
948
+ '-t',
949
+ ghcrImage,
950
+ screenciPackageRoot,
951
+ ]);
952
+ await spawnSilent(containerRuntime, [
953
+ 'build',
954
+ '-f',
955
+ dockerfilePath,
956
+ '-t',
957
+ 'screenci',
958
+ configDir,
959
+ ]);
960
+ completeInline('Building image ✓');
961
+ }
962
+ catch (err) {
963
+ process.stdout.write('\n');
964
+ const msg = err instanceof Error ? err.message : String(err);
965
+ logger.error(msg);
966
+ logger.error('Run again with --verbose to see the full build output');
967
+ process.exit(1);
968
+ }
969
+ }
970
+ }
971
+ if (imageTag !== undefined || process.env['SCREENCI_LOCAL_IMAGE']) {
972
+ await buildImage(containerRuntime, ['build', '-f', dockerfilePath, '-t', 'screenci', configDir], 'Building image', verbose);
973
+ }
797
974
  clearDirectory(resolve(configDir, '.screenci'));
798
- logger.info('Running recording in container...');
799
- await spawnInherited(containerRuntime, [
975
+ const secret = process.env['SCREENCI_SECRET'];
976
+ if (secret === undefined) {
977
+ logger.error('Error: SCREENCI_SECRET is not set');
978
+ process.exit(1);
979
+ }
980
+ await spawnContainerRecording(containerRuntime, [
800
981
  'run',
801
982
  '--rm',
802
983
  '-e',
803
984
  'SCREENCI_IN_CONTAINER=true',
804
985
  '-e',
805
986
  'SCREENCI_RECORD=true',
987
+ '-e',
988
+ `SCREENCI_SECRET=${secret}`,
806
989
  '-v',
807
990
  `${configDir}/.screenci:/app/.screenci`,
808
991
  '-v',
@@ -834,8 +1017,10 @@ async function run(command, additionalArgs, customConfigPath) {
834
1017
  const isHeaded = additionalArgs.includes('--headed');
835
1018
  const shouldUseUI = command === 'dev' && !isHeaded;
836
1019
  const mode = command === 'dev' ? (isHeaded ? 'headed mode' : 'UI mode') : 'recorder';
837
- logger.info(`Running ScreenCI ${mode} with npx...`);
838
- logger.info(`Using config: ${configPath}`);
1020
+ if (process.env.SCREENCI_IN_CONTAINER !== 'true') {
1021
+ logger.info(`Running ScreenCI ${mode} with npx...`);
1022
+ logger.info(`Using config: ${configPath}`);
1023
+ }
839
1024
  const playwrightArgs = [
840
1025
  'playwright',
841
1026
  'test',