screenci 0.0.34 → 0.0.37

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.
package/dist/cli.js CHANGED
@@ -1,14 +1,13 @@
1
- #!/usr/bin/env node
2
- import { spawn, spawnSync } from 'child_process';
1
+ import { spawn } from 'child_process';
3
2
  import { createReadStream } from 'fs';
4
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, } from 'fs';
3
+ import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync } from 'fs';
5
4
  import { createHash } from 'crypto';
6
5
  import { createServer } from 'http';
7
6
  import { appendFile, mkdir, readdir, readFile, stat, writeFile, } from 'fs/promises';
8
7
  import { dirname, relative as pathRelative, resolve } from 'path';
9
8
  import { fileURLToPath } from 'url';
10
9
  import { Command, CommanderError } from 'commander';
11
- import { input, confirm, select } from '@inquirer/prompts';
10
+ import { input, confirm } from '@inquirer/prompts';
12
11
  import ora from 'ora';
13
12
  import pc from 'picocolors';
14
13
  import { logger } from './src/logger.js';
@@ -107,68 +106,6 @@ function createUploadAbortController(activityLabel) {
107
106
  cleanup,
108
107
  };
109
108
  }
110
- function parseDockerfileVersion(dockerfilePath) {
111
- let content;
112
- try {
113
- content = readFileSync(dockerfilePath, 'utf-8');
114
- }
115
- catch {
116
- return 'unknown';
117
- }
118
- const fromLine = content
119
- .split('\n')
120
- .find((line) => line.trim().toUpperCase().startsWith('FROM'));
121
- if (!fromLine)
122
- return 'unknown';
123
- const match = fromLine.match(/:([^\s@]+)/);
124
- return match?.[1] ?? 'unknown';
125
- }
126
- const CONTAINER_RUNTIME_DOCS_URL = 'https://screenci.com/docs/guides/getting-started/#prerequisites';
127
- function prerequisitesMessage() {
128
- return `See prerequisites: ${pc.blue(CONTAINER_RUNTIME_DOCS_URL)}`;
129
- }
130
- const MIN_CONTAINER_RUNTIME_MAJOR_VERSION = {
131
- podman: 3,
132
- docker: 27,
133
- };
134
- function parseContainerRuntimeMajorVersion(versionOutput) {
135
- const match = versionOutput.match(/(\d+)(?:\.\d+){0,2}/);
136
- if (!match)
137
- return null;
138
- const majorVersion = Number.parseInt(match[1] ?? '', 10);
139
- return Number.isNaN(majorVersion) ? null : majorVersion;
140
- }
141
- function checkContainerRuntime(runtime) {
142
- const result = spawnSync(runtime, ['--version'], { encoding: 'utf8' });
143
- if (result.status !== 0 || result.error !== undefined) {
144
- return null;
145
- }
146
- const version = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
147
- return {
148
- runtime,
149
- version,
150
- majorVersion: parseContainerRuntimeMajorVersion(version),
151
- };
152
- }
153
- function getPreferredContainerRuntime() {
154
- const podman = checkContainerRuntime('podman');
155
- if (podman)
156
- return podman;
157
- return checkContainerRuntime('docker');
158
- }
159
- function exitContainerRuntimeNotFound(runtime) {
160
- logger.error(`Error: ${runtime} not found.`);
161
- logger.error(`Install ${runtime} or remove the --${runtime} flag.`);
162
- logger.error(prerequisitesMessage());
163
- process.exit(1);
164
- }
165
- function warnIfContainerRuntimeVersionIsOld(runtimeCheck) {
166
- const minimumVersion = MIN_CONTAINER_RUNTIME_MAJOR_VERSION[runtimeCheck.runtime];
167
- if (runtimeCheck.majorVersion !== null &&
168
- runtimeCheck.majorVersion < minimumVersion) {
169
- logger.warn(`Your ${runtimeCheck.runtime} version (${runtimeCheck.version}) is quite old. We recommend updating. ${prerequisitesMessage()}`);
170
- }
171
- }
172
109
  function spawnSilent(cmd, args, cwd) {
173
110
  return new Promise((resolve, reject) => {
174
111
  const child = spawn(cmd, args, { stdio: 'pipe', ...(cwd ? { cwd } : {}) });
@@ -235,58 +172,6 @@ function forwardChildSignals(child, activityLabel) {
235
172
  getForwardedSignal: () => forwardedSignal,
236
173
  };
237
174
  }
238
- const CONTAINER_LOG_FILTER = [
239
- /^Running ScreenCI /,
240
- /^Using config:/,
241
- /^Starting Xvfb /,
242
- /^Xvfb started /,
243
- /^Recording video to:/,
244
- /^Recording with /,
245
- /^Stopping recording\.\.\./,
246
- /^FFmpeg exited /,
247
- /^Video saved to:/,
248
- /^Events saved to:/,
249
- ];
250
- function spawnContainerRecording(cmd, args) {
251
- return new Promise((resolve, reject) => {
252
- const child = spawn(cmd, args, { stdio: ['inherit', 'pipe', 'pipe'] });
253
- const childSignals = forwardChildSignals(child, 'screenci record');
254
- function forwardFiltered(chunk, out) {
255
- const lines = chunk.toString().split('\n');
256
- for (const line of lines) {
257
- if (line === '')
258
- continue;
259
- if (!CONTAINER_LOG_FILTER.some((re) => re.test(line.trimStart()))) {
260
- out.write(line + '\n');
261
- }
262
- }
263
- }
264
- child.stdout?.on('data', (chunk) => forwardFiltered(chunk, process.stdout));
265
- child.stderr?.on('data', (chunk) => forwardFiltered(chunk, process.stderr));
266
- child.on('close', (code, signal) => {
267
- const forwardedSignal = childSignals.getForwardedSignal();
268
- childSignals.cleanup();
269
- if (forwardedSignal) {
270
- process.kill(process.pid, forwardedSignal);
271
- return;
272
- }
273
- if (signal) {
274
- process.kill(process.pid, signal);
275
- return;
276
- }
277
- else if (code === 0) {
278
- resolve();
279
- }
280
- else {
281
- reject(new Error(`${cmd} exited with code ${code}`));
282
- }
283
- });
284
- child.on('error', (err) => {
285
- childSignals.cleanup();
286
- reject(err);
287
- });
288
- });
289
- }
290
175
  function clearDirectory(dir) {
291
176
  mkdirSync(dir, { recursive: true });
292
177
  for (const entry of readdirSync(dir)) {
@@ -876,8 +761,6 @@ async function loadScreenCIConfigAndEnv(configPath) {
876
761
  return { resolvedConfigPath, screenciConfig };
877
762
  }
878
763
  function loadEnvFile(envFilePath, warnOnFailure) {
879
- if (process.env.CI)
880
- return;
881
764
  try {
882
765
  process.loadEnvFile(envFilePath);
883
766
  }
@@ -887,6 +770,19 @@ function loadEnvFile(envFilePath, warnOnFailure) {
887
770
  }
888
771
  }
889
772
  }
773
+ async function loadEnvFileFromConfigSource(resolvedConfigPath, warnOnFailure) {
774
+ try {
775
+ const screenciConfig = await tryReadConfigFromSource(resolvedConfigPath);
776
+ if (screenciConfig.envFile) {
777
+ const envFilePath = resolve(dirname(resolvedConfigPath), screenciConfig.envFile);
778
+ loadEnvFile(envFilePath, warnOnFailure);
779
+ }
780
+ }
781
+ catch {
782
+ // Config import may require Playwright context or dynamic values. Continue with
783
+ // the existing process env; Playwright will still load the config normally.
784
+ }
785
+ }
890
786
  export function extractConfigStringLiteral(configSource, property) {
891
787
  const singleQuoteMatch = configSource.match(new RegExp(property + "\\s*:\\s*'([^'\\n]+)'"));
892
788
  if (singleQuoteMatch)
@@ -971,11 +867,7 @@ async function updateVideoVisibility(videoId, isPublic, configPath) {
971
867
  }
972
868
  logger.info(`${isPublic ? 'Made public' : 'Made private'}: ${videoId}`);
973
869
  }
974
- function generateConfig(projectName, initTarget) {
975
- const baseURLBlock = initTarget
976
- ? ` baseURL: ${JSON.stringify(initTarget.baseURL)},
977
- `
978
- : '';
870
+ function generateConfig(projectName) {
979
871
  return `import { defineConfig } from 'screenci'
980
872
 
981
873
  export default defineConfig({
@@ -983,7 +875,7 @@ export default defineConfig({
983
875
  envFile: '.env',
984
876
  videoDir: './videos',
985
877
  use: {
986
- ${baseURLBlock} recordOptions: {
878
+ recordOptions: {
987
879
  aspectRatio: '16:9',
988
880
  quality: '1080p',
989
881
  fps: 30,
@@ -997,13 +889,12 @@ ${baseURLBlock} recordOptions: {
997
889
  })
998
890
  `;
999
891
  }
1000
- function generatePackageJson(packageName, includePlaywrightCli = false, screenciDependency = 'latest') {
892
+ function generatePackageJson(includePlaywrightCli = false, screenciDependency = 'latest') {
1001
893
  const devDependencies = {};
1002
894
  if (includePlaywrightCli) {
1003
895
  devDependencies['@playwright/cli'] = 'latest';
1004
896
  }
1005
897
  return (JSON.stringify({
1006
- name: packageName,
1007
898
  type: 'module',
1008
899
  scripts: {
1009
900
  record: 'screenci record',
@@ -1012,6 +903,7 @@ function generatePackageJson(packageName, includePlaywrightCli = false, screenci
1012
903
  },
1013
904
  dependencies: {
1014
905
  screenci: screenciDependency,
906
+ '@playwright/test': '^1.59.0',
1015
907
  },
1016
908
  devDependencies,
1017
909
  }, null, 2) + '\n');
@@ -1075,15 +967,6 @@ Learn more: https://screenci.com/docs/intro/
1075
967
  4. View results on screenci.com and optionally enable a public URL to embed the video on your site.
1076
968
  `;
1077
969
  }
1078
- function generateDockerfile() {
1079
- return `FROM ghcr.io/screenci/record:latest
1080
-
1081
- COPY package.json ./
1082
- RUN npm install
1083
- COPY screenci.config.ts ./
1084
- COPY videos ./videos
1085
- `;
1086
- }
1087
970
  function generateGitignore() {
1088
971
  return `/playwright-report/
1089
972
  .screenci
@@ -1093,7 +976,7 @@ node_modules/
1093
976
  `;
1094
977
  }
1095
978
  function generateGithubAction() {
1096
- return `name: Record
979
+ return `name: ScreenCI
1097
980
 
1098
981
  on:
1099
982
  push:
@@ -1112,21 +995,31 @@ jobs:
1112
995
  SCREENCI_SECRET: \${{ secrets.SCREENCI_SECRET }}
1113
996
  run: |
1114
997
  if [ -z "$SCREENCI_SECRET" ]; then
1115
- echo "::error::SCREENCI_SECRET is not set. Copy it from https://app.screenci.com/secrets and add it under Settings → Secrets and variables → Actions → Repository secrets, and then rerun this action."
998
+ echo "::error::SCREENCI_SECRET is not set. Copy it from https://app.screenci.com/secrets or ./screenci/.env, add it under Settings → Secrets and variables → Actions → Repository secrets, and then rerun this action."
1116
999
  exit 1
1117
1000
  fi
1118
1001
 
1119
1002
  - uses: actions/checkout@v4
1120
1003
 
1121
- - uses: actions/setup-node@v6
1004
+ - uses: actions/setup-node@v4
1122
1005
  with:
1123
- node-version: latest
1006
+ node-version: 24
1007
+ cache: npm
1008
+ cache-dependency-path: screenci/package-lock.json
1124
1009
 
1125
1010
  - name: Install dependencies
1126
1011
  working-directory: screenci
1127
- run: npm install --include=dev
1012
+ run: npm ci
1013
+
1014
+ - name: Cache Playwright Chromium
1015
+ uses: actions/cache@v4
1016
+ id: pw-cache
1017
+ with:
1018
+ path: ~/.cache/ms-playwright
1019
+ key: playwright-\${{ runner.os }}-\${{ hashFiles('screenci/package-lock.json') }}
1128
1020
 
1129
1021
  - name: Install Chromium
1022
+ if: steps.pw-cache.outputs.cache-hit != 'true'
1130
1023
  working-directory: screenci
1131
1024
  run: npx playwright install chromium --with-deps
1132
1025
 
@@ -1196,15 +1089,11 @@ const narration = createNarration({
1196
1089
  languages: {
1197
1090
  en: {
1198
1091
  cues: {
1199
- intro:
1200
- 'This example opens your app at the configured base URL first, then uses ScreenCI [pronounce: screen see eye] to show the next steps for creating your first videos.',
1201
1092
  docs: 'Use the guide sidebar to open the AI-Supported Editing guide and review the next steps for writing your own videos.',
1202
1093
  },
1203
1094
  },
1204
1095
  es: {
1205
1096
  cues: {
1206
- intro:
1207
- 'Este ejemplo abre primero tu aplicacion en la URL base configurada y despues usa ScreenCI [pronounce: screen see eye] para mostrar los siguientes pasos para crear tus primeros videos.',
1208
1097
  docs: 'Usa la barra lateral de guias para abrir la guia de edicion asistida por IA y revisar los siguientes pasos para escribir tus propios videos.',
1209
1098
  },
1210
1099
  },
@@ -1212,14 +1101,6 @@ const narration = createNarration({
1212
1101
  })
1213
1102
 
1214
1103
  video('See the next steps in ScreenCI docs', async ({ page }) => {
1215
- await hide(async () => {
1216
- await page.goto('/')
1217
- await page.waitForLoadState('domcontentloaded')
1218
- })
1219
-
1220
- await narration.intro
1221
- await page.waitForTimeout(1000)
1222
-
1223
1104
  await hide(async () => {
1224
1105
  await page.goto('https://screenci.com/')
1225
1106
  await page.getByText('ScreenCI', { exact: true }).first().waitFor()
@@ -1262,95 +1143,9 @@ async function promptInitGithubActionCi() {
1262
1143
  default: true,
1263
1144
  });
1264
1145
  }
1265
- async function promptInitTargetMode() {
1266
- return select({
1267
- message: 'Should videos run against a local development server or a public URL?',
1268
- choices: [
1269
- { name: 'Local development server', value: 'local' },
1270
- { name: 'Public URL', value: 'public' },
1271
- ],
1272
- default: 'local',
1273
- });
1274
- }
1275
- async function promptInitTargetUrl(mode) {
1276
- return input({
1277
- message: mode === 'local' ? 'Local development server URL:' : 'Public app URL:',
1278
- default: mode === 'local' ? 'http://localhost:3000' : 'https://screenci.com',
1279
- });
1280
- }
1281
- function normalizeInitUrl(url) {
1282
- try {
1283
- return new URL(url.trim()).toString();
1284
- }
1285
- catch {
1286
- logger.error(`Error: Invalid URL "${url}"`);
1287
- process.exit(1);
1288
- }
1289
- }
1290
1146
  function getInitProjectRoot() {
1291
1147
  return process.env['SCREENCI_INIT_CWD'] ?? process.cwd();
1292
1148
  }
1293
- function isSourceCliEntrypoint(entrypoint) {
1294
- return (entrypoint?.endsWith('/cli.ts') === true ||
1295
- entrypoint?.endsWith('\\cli.ts') === true);
1296
- }
1297
- function getDevScreenciPackageRoot() {
1298
- const explicitRoot = process.env.SCREENCI_DEV_PACKAGE_ROOT?.trim();
1299
- if (explicitRoot)
1300
- return resolve(explicitRoot);
1301
- if (!isSourceCliEntrypoint(process.argv[1]))
1302
- return undefined;
1303
- return dirname(fileURLToPath(import.meta.url));
1304
- }
1305
- function getLocalScreenciDependency(packageRoot, projectDir) {
1306
- const relativePath = pathRelative(projectDir, packageRoot) || '.';
1307
- const normalizedPath = relativePath.replace(/\\/g, '/');
1308
- return `file:${normalizedPath.startsWith('.') ? normalizedPath : `./${normalizedPath}`}`;
1309
- }
1310
- async function buildLocalScreenciPackage(packageRoot) {
1311
- logger.info(`Using local screenci package: ${packageRoot}`);
1312
- logger.info("Running 'npm run build' for local screenci package...");
1313
- await spawnInherited('npm', ['run', 'build'], packageRoot, 'screenci dev build');
1314
- }
1315
- async function installLocalScreenciPackage(projectDir, packageRoot) {
1316
- const packageJsonPath = resolve(projectDir, 'package.json');
1317
- const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
1318
- packageJson.dependencies = {
1319
- ...packageJson.dependencies,
1320
- screenci: getLocalScreenciDependency(packageRoot, projectDir),
1321
- };
1322
- await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
1323
- logger.info('Installing local screenci package into this project...');
1324
- await spawnInherited('npm', ['install', '--install-links'], projectDir, 'screenci dev install');
1325
- }
1326
- function buildChildEnv() {
1327
- const { PATH, HOME, USER, LOGNAME, TMPDIR, TEMP, TMP, CI } = process.env;
1328
- return {
1329
- PATH,
1330
- HOME,
1331
- USER,
1332
- LOGNAME,
1333
- TMPDIR,
1334
- TEMP,
1335
- TMP,
1336
- CI,
1337
- SCREENCI_SECRET: process.env.SCREENCI_SECRET,
1338
- SCREENCI_INIT_CWD: process.env.SCREENCI_INIT_CWD,
1339
- DEV_FRONTEND_PORT: process.env.DEV_FRONTEND_PORT,
1340
- DEV_BACKEND_PORT: process.env.DEV_BACKEND_PORT,
1341
- SCREENCI_LOCAL_IMAGE: process.env.SCREENCI_LOCAL_IMAGE,
1342
- SCREENCI_IN_CONTAINER: process.env.SCREENCI_IN_CONTAINER,
1343
- SCREENCI_RECORD: process.env.SCREENCI_RECORD,
1344
- SCREENCI_SIGNAL_LOGGING: process.env.SCREENCI_SIGNAL_LOGGING,
1345
- SCREENCI_DEV_PACKAGE_ROOT: process.env.SCREENCI_DEV_PACKAGE_ROOT,
1346
- };
1347
- }
1348
- function requireContainerRuntime() {
1349
- const runtime = detectContainerRuntime();
1350
- if (runtime === 'podman' || runtime === 'docker')
1351
- return runtime;
1352
- throw new Error('No container runtime available');
1353
- }
1354
1149
  async function runInitAuth() {
1355
1150
  const appUrl = getDevFrontendUrl();
1356
1151
  try {
@@ -1397,7 +1192,6 @@ function checkNodeVersion() {
1397
1192
  async function runInit(projectNameArg, options) {
1398
1193
  const { verbose, install, yes, skill, ci } = options;
1399
1194
  checkNodeVersion();
1400
- checkContainerRuntimeForInit();
1401
1195
  const initCwd = getInitProjectRoot();
1402
1196
  let projectName = projectNameArg?.trim();
1403
1197
  if (!projectName) {
@@ -1416,16 +1210,6 @@ async function runInit(projectNameArg, options) {
1416
1210
  logger.error(`Error: Directory "${dirName}" already exists`);
1417
1211
  process.exit(1);
1418
1212
  }
1419
- const initTarget = yes
1420
- ? undefined
1421
- : await (async () => {
1422
- const mode = await promptInitTargetMode();
1423
- const baseURL = normalizeInitUrl(await promptInitTargetUrl(mode));
1424
- return {
1425
- mode,
1426
- baseURL,
1427
- };
1428
- })();
1429
1213
  const shouldInstallDependencies = yes
1430
1214
  ? true
1431
1215
  : install
@@ -1456,13 +1240,7 @@ async function runInit(projectNameArg, options) {
1456
1240
  '-y',
1457
1241
  ];
1458
1242
  const skillsCommand = `npx ${skillsArgs.join(' ')}`;
1459
- const devScreenciPackageRoot = getDevScreenciPackageRoot();
1460
- const screenciDependency = devScreenciPackageRoot
1461
- ? getLocalScreenciDependency(devScreenciPackageRoot, projectDir)
1462
- : await readCurrentScreenciVersion();
1463
- if (devScreenciPackageRoot) {
1464
- await buildLocalScreenciPackage(devScreenciPackageRoot);
1465
- }
1243
+ const screenciDependency = await readCurrentScreenciVersion();
1466
1244
  await mkdir(resolve(projectDir, 'videos'), { recursive: true });
1467
1245
  if (shouldAddGithubActionCi) {
1468
1246
  if (!existsSync(githubDir)) {
@@ -1472,11 +1250,10 @@ async function runInit(projectNameArg, options) {
1472
1250
  await mkdir(githubWorkflowsDir);
1473
1251
  }
1474
1252
  }
1475
- await writeFile(resolve(projectDir, 'screenci.config.ts'), generateConfig(projectName, initTarget));
1476
- await writeFile(resolve(projectDir, 'package.json'), generatePackageJson(dirName, shouldAddPlaywrightCli, screenciDependency));
1253
+ await writeFile(resolve(projectDir, 'screenci.config.ts'), generateConfig(projectName));
1254
+ await writeFile(resolve(projectDir, 'package.json'), generatePackageJson(shouldAddPlaywrightCli, screenciDependency));
1477
1255
  await writeFile(resolve(projectDir, 'tsconfig.json'), generateTsconfig());
1478
1256
  await writeFile(resolve(projectDir, 'README.md'), generateReadme(projectName));
1479
- await writeFile(resolve(projectDir, 'Dockerfile'), generateDockerfile());
1480
1257
  await writeFile(resolve(projectDir, '.gitignore'), generateGitignore());
1481
1258
  await writeFile(resolve(projectDir, 'videos', 'example.video.ts'), generateExampleVideo());
1482
1259
  if (shouldAddGithubActionCi) {
@@ -1489,7 +1266,6 @@ async function runInit(projectNameArg, options) {
1489
1266
  logger.info(' package.json');
1490
1267
  logger.info(' tsconfig.json');
1491
1268
  logger.info(' README.md');
1492
- logger.info(' Dockerfile');
1493
1269
  logger.info(' .gitignore');
1494
1270
  logger.info(' videos/example.video.ts');
1495
1271
  if (shouldAddGithubActionCi) {
@@ -1514,24 +1290,14 @@ async function runInit(projectNameArg, options) {
1514
1290
  }
1515
1291
  }
1516
1292
  if (verbose) {
1517
- const installArgs = devScreenciPackageRoot
1518
- ? ['install', '--include=dev', '--install-links']
1519
- : ['install', '--include=dev'];
1293
+ const installArgs = ['install', '--include=dev'];
1520
1294
  logger.info(`Running 'npm ${installArgs.join(' ')}'...`);
1521
1295
  await spawnInherited('npm', installArgs, projectDir, 'screenci init');
1522
1296
  }
1523
1297
  else {
1524
1298
  const spinner = ora('Running npm install...').start();
1525
1299
  try {
1526
- const installArgs = devScreenciPackageRoot
1527
- ? [
1528
- 'install',
1529
- '--include=dev',
1530
- '--install-links',
1531
- '--prefix',
1532
- projectDir,
1533
- ]
1534
- : ['install', '--include=dev', '--prefix', projectDir];
1300
+ const installArgs = ['install', '--include=dev', '--prefix', projectDir];
1535
1301
  await spawnSilent('npm', installArgs);
1536
1302
  spinner.succeed('npm install complete');
1537
1303
  }
@@ -1575,42 +1341,8 @@ export async function main() {
1575
1341
  .allowUnknownOption(true)
1576
1342
  .action(async () => {
1577
1343
  const parsed = parseRecordCliArgs(getSubcommandArgv('record'));
1578
- if (parsed.forcedRuntime === 'both') {
1579
- logger.error('Error: --podman and --docker cannot be used together');
1580
- process.exit(1);
1581
- }
1582
- const useContainer = process.env.SCREENCI_IN_CONTAINER !== 'true';
1583
- // Validate early so we don't build the container unnecessarily
1584
- if (useContainer) {
1585
- validateArgs(parsed.otherArgs);
1586
- }
1587
- // On the host, load .env so SCREENCI_SECRET is available for uploads
1588
- if (process.env.SCREENCI_IN_CONTAINER !== 'true') {
1589
- const resolvedConfigForSecret = findScreenCIConfig(parsed.configPath);
1590
- if (resolvedConfigForSecret) {
1591
- try {
1592
- const screenciConfig = await loadRecordConfigWithoutPlaywrightCollision(resolvedConfigForSecret);
1593
- if (screenciConfig.envFile) {
1594
- const envFilePath = resolve(dirname(resolvedConfigForSecret), screenciConfig.envFile);
1595
- loadEnvFile(envFilePath, false);
1596
- }
1597
- }
1598
- catch {
1599
- // Config import failed — continue with whatever is already in env
1600
- }
1601
- }
1602
- }
1603
- if (useContainer) {
1604
- await ensureScreenciSecret();
1605
- }
1606
- if (useContainer) {
1607
- await runWithContainer(parsed.otherArgs, parsed.configPath, parsed.verbose, parsed.forcedRuntime);
1608
- }
1609
- else {
1610
- await run('record', parsed.otherArgs, parsed.configPath);
1611
- }
1612
- // Upload only from the host, not from inside the container
1613
- if (process.env.SCREENCI_IN_CONTAINER === 'true')
1344
+ await run('record', parsed.otherArgs, parsed.configPath);
1345
+ if (process.env.SCREENCI_RECORDING === 'true')
1614
1346
  return;
1615
1347
  // After recording, upload results to API if configured
1616
1348
  const resolvedConfigPath = findScreenCIConfig(parsed.configPath);
@@ -1632,6 +1364,7 @@ export async function main() {
1632
1364
  const screenciDir = resolve(configDir, '.screenci');
1633
1365
  let projectId = null;
1634
1366
  try {
1367
+ logger.info('');
1635
1368
  projectId = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, secret);
1636
1369
  }
1637
1370
  catch (err) {
@@ -1659,9 +1392,23 @@ export async function main() {
1659
1392
  .allowUnknownOption(true)
1660
1393
  .action(async () => {
1661
1394
  const parsed = parseConfigCliArgs(getSubcommandArgv('test'));
1395
+ const resolvedConfigPath = findScreenCIConfig(parsed.configPath);
1396
+ if (resolvedConfigPath) {
1397
+ try {
1398
+ const screenciConfig = await loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath);
1399
+ if (screenciConfig.envFile) {
1400
+ const envFilePath = resolve(dirname(resolvedConfigPath), screenciConfig.envFile);
1401
+ loadEnvFile(envFilePath, true);
1402
+ }
1403
+ }
1404
+ catch (err) {
1405
+ logger.warn('Failed to load config for test env:', err);
1406
+ }
1407
+ }
1662
1408
  await run('test', parsed.otherArgs, parsed.configPath);
1663
- if (process.env.SCREENCI_IN_CONTAINER === 'true')
1409
+ if (process.env.SCREENCI_RECORDING === 'true')
1664
1410
  return;
1411
+ logger.info('');
1665
1412
  logger.info(`Tests passed. Run ${pc.cyan('npx screenci record')} to render the videos.`);
1666
1413
  });
1667
1414
  program
@@ -1756,7 +1503,6 @@ function getSubcommandArgv(command) {
1756
1503
  function parseRecordCliArgs(args) {
1757
1504
  let configPath;
1758
1505
  let verbose = false;
1759
- let forcedRuntime;
1760
1506
  const otherArgs = [];
1761
1507
  for (let i = 0; i < args.length; i++) {
1762
1508
  const arg = args[i];
@@ -1774,12 +1520,6 @@ function parseRecordCliArgs(args) {
1774
1520
  else if (arg === '--verbose' || arg === '-v') {
1775
1521
  verbose = true;
1776
1522
  }
1777
- else if (arg === '--podman') {
1778
- forcedRuntime = forcedRuntime === 'docker' ? 'both' : 'podman';
1779
- }
1780
- else if (arg === '--docker') {
1781
- forcedRuntime = forcedRuntime === 'podman' ? 'both' : 'docker';
1782
- }
1783
1523
  else {
1784
1524
  otherArgs.push(arg);
1785
1525
  }
@@ -1787,7 +1527,6 @@ function parseRecordCliArgs(args) {
1787
1527
  return {
1788
1528
  configPath,
1789
1529
  verbose,
1790
- forcedRuntime,
1791
1530
  otherArgs,
1792
1531
  };
1793
1532
  }
@@ -1860,122 +1599,6 @@ function spawnInherited(cmd, args, cwd, activityLabel = cmd) {
1860
1599
  });
1861
1600
  });
1862
1601
  }
1863
- export function detectContainerRuntime(forcedRuntime) {
1864
- const runtimeCheck = forcedRuntime
1865
- ? checkContainerRuntime(forcedRuntime)
1866
- : getPreferredContainerRuntime();
1867
- if (runtimeCheck) {
1868
- warnIfContainerRuntimeVersionIsOld(runtimeCheck);
1869
- return runtimeCheck.runtime;
1870
- }
1871
- if (forcedRuntime) {
1872
- exitContainerRuntimeNotFound(forcedRuntime);
1873
- }
1874
- logger.error('Error: Neither podman nor docker found.');
1875
- logger.error('Please install podman (recommended) or docker to use screenci record.');
1876
- logger.error(prerequisitesMessage());
1877
- process.exit(1);
1878
- }
1879
- function checkContainerRuntimeForInit() {
1880
- const runtimeCheck = getPreferredContainerRuntime();
1881
- if (!runtimeCheck) {
1882
- logger.warn('Neither podman nor docker found. Install one before running screenci record.');
1883
- logger.warn(prerequisitesMessage());
1884
- return;
1885
- }
1886
- warnIfContainerRuntimeVersionIsOld(runtimeCheck);
1887
- }
1888
- async function buildProjectImage(containerRuntime, dockerfilePath, configDir, verbose) {
1889
- if (verbose) {
1890
- await spawnInherited(containerRuntime, ['build', '-f', dockerfilePath, '-t', 'screenci', configDir], undefined, 'Building project image');
1891
- return;
1892
- }
1893
- await spawnSilent(containerRuntime, [
1894
- 'build',
1895
- '-f',
1896
- dockerfilePath,
1897
- '-t',
1898
- 'screenci',
1899
- configDir,
1900
- ]);
1901
- }
1902
- async function runWithContainer(additionalArgs, customConfigPath, verbose = false, forcedRuntime) {
1903
- const configPath = findScreenCIConfig(customConfigPath);
1904
- if (!configPath) {
1905
- const errorMsg = customConfigPath
1906
- ? `Error: Config file not found: ${customConfigPath}`
1907
- : 'Error: screenci.config.ts not found in current directory';
1908
- logger.error(errorMsg);
1909
- process.exit(1);
1910
- }
1911
- const configDir = dirname(configPath);
1912
- const repoRoot = findRepoRoot(configDir);
1913
- if (!repoRoot) {
1914
- logger.error('Error: Could not find repository root (.git or pnpm-workspace.yaml)');
1915
- process.exit(1);
1916
- }
1917
- logger.info('Preparing ScreenCI recording container...');
1918
- const containerRuntime = detectContainerRuntime(forcedRuntime);
1919
- const imageName = process.env['SCREENCI_LOCAL_IMAGE']
1920
- ? 'screenci'
1921
- : 'ghcr.io/screenci/record:latest';
1922
- logger.info(`Using ${containerRuntime} with image ${imageName}`);
1923
- if (process.env['SCREENCI_LOCAL_IMAGE']) {
1924
- logger.info('SCREENCI_LOCAL_IMAGE set — skipping screenci image build');
1925
- }
1926
- else {
1927
- const imageExists = spawnSync(containerRuntime, ['image', 'exists', imageName], {
1928
- stdio: 'ignore',
1929
- }).status === 0;
1930
- if (!imageExists) {
1931
- logger.info(`Image ${imageName} not found locally, pulling...`);
1932
- if (verbose) {
1933
- await spawnInherited(containerRuntime, ['pull', imageName]);
1934
- }
1935
- else {
1936
- await spawnSilent(containerRuntime, ['pull', imageName]);
1937
- }
1938
- }
1939
- }
1940
- clearDirectory(resolve(configDir, '.screenci'));
1941
- const secret = process.env['SCREENCI_SECRET'];
1942
- if (secret === undefined) {
1943
- logger.error('Error: SCREENCI_SECRET is not set');
1944
- process.exit(1);
1945
- }
1946
- logger.info('Starting ScreenCI recording container...');
1947
- const containerBaseHost = containerRuntime === 'docker'
1948
- ? 'host.docker.internal'
1949
- : 'host.containers.internal';
1950
- await spawnContainerRecording(containerRuntime, [
1951
- 'run',
1952
- '--rm',
1953
- ...(containerRuntime === 'docker'
1954
- ? ['--add-host', 'host.docker.internal:host-gateway']
1955
- : []),
1956
- ...(process.env.CI !== undefined ? ['-e', `CI=${process.env.CI}`] : []),
1957
- '-e',
1958
- 'SCREENCI_IN_CONTAINER=true',
1959
- '-e',
1960
- `SCREENCI_CONTAINER_BASE_HOST=${containerBaseHost}`,
1961
- '-e',
1962
- 'SCREENCI_RECORD=true',
1963
- '-e',
1964
- `SCREENCI_SECRET=${secret}`,
1965
- '-e',
1966
- 'SCREENCI_SIGNAL_LOGGING=silent',
1967
- '-v',
1968
- `${configDir}/.screenci:/app/.screenci`,
1969
- '-v',
1970
- `${configPath}:/app/screenci.config.ts`,
1971
- '-v',
1972
- `${configDir}/videos:/app/videos`,
1973
- imageName,
1974
- 'screenci',
1975
- 'record',
1976
- ...additionalArgs,
1977
- ]);
1978
- }
1979
1602
  async function run(command, additionalArgs, customConfigPath) {
1980
1603
  const configPath = findScreenCIConfig(customConfigPath);
1981
1604
  if (!configPath) {
@@ -1985,6 +1608,10 @@ async function run(command, additionalArgs, customConfigPath) {
1985
1608
  logger.error(errorMsg);
1986
1609
  process.exit(1);
1987
1610
  }
1611
+ if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
1612
+ await loadEnvFileFromConfigSource(configPath, false);
1613
+ }
1614
+ const envForChild = { ...process.env };
1988
1615
  // Only validate args for record command
1989
1616
  if (command === 'record') {
1990
1617
  await ensureScreenciSecret();
@@ -1992,30 +1619,16 @@ async function run(command, additionalArgs, customConfigPath) {
1992
1619
  const screenciDir = resolve(dirname(configPath), '.screenci');
1993
1620
  clearDirectory(screenciDir);
1994
1621
  }
1995
- const mode = command === 'test' ? 'tests' : 'recorder';
1996
- if (process.env.SCREENCI_IN_CONTAINER !== 'true') {
1997
- logger.info(`Running ScreenCI ${mode} with npx...`);
1622
+ if (process.env.SCREENCI_RECORDING !== 'true') {
1998
1623
  logger.info(`Using config: ${configPath}`);
1999
1624
  }
2000
- const devScreenciPackageRoot = getDevScreenciPackageRoot();
2001
- if (devScreenciPackageRoot) {
2002
- const configDir = dirname(configPath);
2003
- await buildLocalScreenciPackage(devScreenciPackageRoot);
2004
- await installLocalScreenciPackage(configDir, devScreenciPackageRoot);
2005
- }
2006
- const playwrightArgs = [
2007
- 'playwright',
2008
- 'test',
2009
- '--config',
2010
- configPath,
2011
- ...additionalArgs,
2012
- ];
2013
- const child = spawn('npx', playwrightArgs, {
1625
+ const playwrightArgs = ['test', '--config', configPath, ...additionalArgs];
1626
+ const child = spawn('playwright', playwrightArgs, {
2014
1627
  stdio: 'inherit',
2015
1628
  env: {
2016
- ...buildChildEnv(),
1629
+ ...envForChild,
2017
1630
  // Enable recording only for record command
2018
- ...(command === 'record' ? { SCREENCI_RECORD: 'true' } : {}),
1631
+ ...(command === 'record' ? { SCREENCI_RECORDING: 'true' } : {}),
2019
1632
  },
2020
1633
  });
2021
1634
  const childSignals = forwardChildSignals(child, `screenci ${command}`);
@@ -2048,8 +1661,11 @@ async function run(command, additionalArgs, customConfigPath) {
2048
1661
  // Check if this module is the main module (handles symlinks properly)
2049
1662
  const currentFile = fileURLToPath(import.meta.url);
2050
1663
  const mainFile = process.argv[1] ? realpathSync(process.argv[1]) : null;
1664
+ const currentRealFile = realpathSync(currentFile);
2051
1665
  if (mainFile &&
2052
- (currentFile === mainFile || currentFile === realpathSync(mainFile))) {
1666
+ (currentFile === mainFile ||
1667
+ currentRealFile === mainFile ||
1668
+ currentFile === realpathSync(mainFile))) {
2053
1669
  main().catch((error) => {
2054
1670
  logger.error('Error:', error.message);
2055
1671
  process.exit(1);