screenci 0.0.34 → 0.0.38

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 (41) hide show
  1. package/README.md +1 -2
  2. package/bin/screenci.js +8 -0
  3. package/dist/cli.d.ts +0 -7
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +121 -465
  6. package/dist/cli.js.map +1 -1
  7. package/dist/src/browserLaunchOptions.d.ts +1 -1
  8. package/dist/src/browserLaunchOptions.d.ts.map +1 -1
  9. package/dist/src/browserLaunchOptions.js +1 -4
  10. package/dist/src/browserLaunchOptions.js.map +1 -1
  11. package/dist/src/config.d.ts +1 -1
  12. package/dist/src/config.d.ts.map +1 -1
  13. package/dist/src/config.js +17 -52
  14. package/dist/src/config.js.map +1 -1
  15. package/dist/src/defaults.d.ts +1 -1
  16. package/dist/src/defaults.js +4 -4
  17. package/dist/src/defaults.js.map +1 -1
  18. package/dist/src/types.d.ts +13 -6
  19. package/dist/src/types.d.ts.map +1 -1
  20. package/dist/src/video.d.ts +0 -1
  21. package/dist/src/video.d.ts.map +1 -1
  22. package/dist/src/video.js +47 -247
  23. package/dist/src/video.js.map +1 -1
  24. package/dist/test-fixtures/env-file.config.d.ts +6 -0
  25. package/dist/test-fixtures/env-file.config.d.ts.map +1 -0
  26. package/dist/test-fixtures/env-file.config.js +5 -0
  27. package/dist/test-fixtures/env-file.config.js.map +1 -0
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +7 -7
  30. package/skills/screenci/SKILL.md +1 -1
  31. package/skills/screenci/references/init.md +2 -3
  32. package/skills/screenci/references/record.md +1 -1
  33. package/dist/Dockerfile +0 -41
  34. package/dist/src/chromiumProfile.d.ts +0 -6
  35. package/dist/src/chromiumProfile.d.ts.map +0 -1
  36. package/dist/src/chromiumProfile.js +0 -20
  37. package/dist/src/chromiumProfile.js.map +0 -1
  38. package/dist/src/xvfb.d.ts +0 -22
  39. package/dist/src/xvfb.d.ts.map +0 -1
  40. package/dist/src/xvfb.js +0 -87
  41. package/dist/src/xvfb.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,6 @@
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';
@@ -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,17 +761,34 @@ 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
  }
884
767
  catch (err) {
885
- if (warnOnFailure) {
768
+ if (warnOnFailure && !isMissingFileError(err)) {
886
769
  logger.warn(`Failed to load env file ${envFilePath}:`, err);
887
770
  }
888
771
  }
889
772
  }
773
+ function isMissingFileError(err) {
774
+ return (typeof err === 'object' &&
775
+ err !== null &&
776
+ 'code' in err &&
777
+ err.code === 'ENOENT');
778
+ }
779
+ async function loadEnvFileFromConfigSource(resolvedConfigPath, warnOnFailure) {
780
+ try {
781
+ const screenciConfig = await tryReadConfigFromSource(resolvedConfigPath);
782
+ if (screenciConfig.envFile) {
783
+ const envFilePath = resolve(dirname(resolvedConfigPath), screenciConfig.envFile);
784
+ loadEnvFile(envFilePath, warnOnFailure);
785
+ }
786
+ }
787
+ catch {
788
+ // Config import may require Playwright context or dynamic values. Continue with
789
+ // the existing process env; Playwright will still load the config normally.
790
+ }
791
+ }
890
792
  export function extractConfigStringLiteral(configSource, property) {
891
793
  const singleQuoteMatch = configSource.match(new RegExp(property + "\\s*:\\s*'([^'\\n]+)'"));
892
794
  if (singleQuoteMatch)
@@ -971,11 +873,7 @@ async function updateVideoVisibility(videoId, isPublic, configPath) {
971
873
  }
972
874
  logger.info(`${isPublic ? 'Made public' : 'Made private'}: ${videoId}`);
973
875
  }
974
- function generateConfig(projectName, initTarget) {
975
- const baseURLBlock = initTarget
976
- ? ` baseURL: ${JSON.stringify(initTarget.baseURL)},
977
- `
978
- : '';
876
+ function generateConfig(projectName) {
979
877
  return `import { defineConfig } from 'screenci'
980
878
 
981
879
  export default defineConfig({
@@ -983,7 +881,7 @@ export default defineConfig({
983
881
  envFile: '.env',
984
882
  videoDir: './videos',
985
883
  use: {
986
- ${baseURLBlock} recordOptions: {
884
+ recordOptions: {
987
885
  aspectRatio: '16:9',
988
886
  quality: '1080p',
989
887
  fps: 30,
@@ -997,13 +895,12 @@ ${baseURLBlock} recordOptions: {
997
895
  })
998
896
  `;
999
897
  }
1000
- function generatePackageJson(packageName, includePlaywrightCli = false, screenciDependency = 'latest') {
898
+ function generatePackageJson(includePlaywrightCli = false, screenciDependency = 'latest') {
1001
899
  const devDependencies = {};
1002
900
  if (includePlaywrightCli) {
1003
901
  devDependencies['@playwright/cli'] = 'latest';
1004
902
  }
1005
903
  return (JSON.stringify({
1006
- name: packageName,
1007
904
  type: 'module',
1008
905
  scripts: {
1009
906
  record: 'screenci record',
@@ -1012,6 +909,7 @@ function generatePackageJson(packageName, includePlaywrightCli = false, screenci
1012
909
  },
1013
910
  dependencies: {
1014
911
  screenci: screenciDependency,
912
+ '@playwright/test': '^1.59.0',
1015
913
  },
1016
914
  devDependencies,
1017
915
  }, null, 2) + '\n');
@@ -1075,15 +973,6 @@ Learn more: https://screenci.com/docs/intro/
1075
973
  4. View results on screenci.com and optionally enable a public URL to embed the video on your site.
1076
974
  `;
1077
975
  }
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
976
  function generateGitignore() {
1088
977
  return `/playwright-report/
1089
978
  .screenci
@@ -1092,8 +981,12 @@ node_modules/
1092
981
  .env
1093
982
  `;
1094
983
  }
1095
- function generateGithubAction() {
1096
- return `name: Record
984
+ function generateGithubAction(workingDirectory) {
985
+ const packageLockPath = workingDirectory === '.'
986
+ ? 'package-lock.json'
987
+ : `${workingDirectory}/package-lock.json`;
988
+ const envFilePath = workingDirectory === '.' ? './.env' : `./${workingDirectory}/.env`;
989
+ return `name: ScreenCI
1097
990
 
1098
991
  on:
1099
992
  push:
@@ -1112,27 +1005,37 @@ jobs:
1112
1005
  SCREENCI_SECRET: \${{ secrets.SCREENCI_SECRET }}
1113
1006
  run: |
1114
1007
  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."
1008
+ echo "::error::SCREENCI_SECRET is not set. Copy it from https://app.screenci.com/secrets or ${envFilePath}, add it under Settings → Secrets and variables → Actions → Repository secrets, and then rerun this action."
1116
1009
  exit 1
1117
1010
  fi
1118
1011
 
1119
1012
  - uses: actions/checkout@v4
1120
1013
 
1121
- - uses: actions/setup-node@v6
1014
+ - uses: actions/setup-node@v4
1122
1015
  with:
1123
- node-version: latest
1016
+ node-version: 24
1017
+ cache: npm
1018
+ cache-dependency-path: ${packageLockPath}
1124
1019
 
1125
1020
  - name: Install dependencies
1126
- working-directory: screenci
1127
- run: npm install --include=dev
1021
+ working-directory: ${workingDirectory}
1022
+ run: npm ci
1023
+
1024
+ - name: Cache Playwright Chromium
1025
+ uses: actions/cache@v5
1026
+ id: pw-cache
1027
+ with:
1028
+ path: ~/.cache/ms-playwright
1029
+ key: playwright-\${{ runner.os }}-\${{ hashFiles('${packageLockPath}') }}
1128
1030
 
1129
1031
  - name: Install Chromium
1130
- working-directory: screenci
1032
+ if: steps.pw-cache.outputs.cache-hit != 'true'
1033
+ working-directory: ${workingDirectory}
1131
1034
  run: npx playwright install chromium --with-deps
1132
1035
 
1133
1036
  - id: record
1134
1037
  name: Record
1135
- working-directory: screenci
1038
+ working-directory: ${workingDirectory}
1136
1039
  env:
1137
1040
  SCREENCI_SECRET: \${{ secrets.SCREENCI_SECRET }}
1138
1041
  run: npm run record
@@ -1196,15 +1099,11 @@ const narration = createNarration({
1196
1099
  languages: {
1197
1100
  en: {
1198
1101
  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
1102
  docs: 'Use the guide sidebar to open the AI-Supported Editing guide and review the next steps for writing your own videos.',
1202
1103
  },
1203
1104
  },
1204
1105
  es: {
1205
1106
  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
1107
  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
1108
  },
1210
1109
  },
@@ -1212,14 +1111,6 @@ const narration = createNarration({
1212
1111
  })
1213
1112
 
1214
1113
  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
1114
  await hide(async () => {
1224
1115
  await page.goto('https://screenci.com/')
1225
1116
  await page.getByText('ScreenCI', { exact: true }).first().waitFor()
@@ -1262,95 +1153,30 @@ async function promptInitGithubActionCi() {
1262
1153
  default: true,
1263
1154
  });
1264
1155
  }
1265
- async function promptInitTargetMode() {
1156
+ async function promptInitRepositoryMode() {
1266
1157
  return select({
1267
- message: 'Should videos run against a local development server or a public URL?',
1158
+ message: 'Initialize ScreenCI as a standalone project or part of the existing repository?',
1159
+ default: 'standalone',
1268
1160
  choices: [
1269
- { name: 'Local development server', value: 'local' },
1270
- { name: 'Public URL', value: 'public' },
1161
+ {
1162
+ name: 'Standalone project',
1163
+ value: 'standalone',
1164
+ description: 'Create a project directory with its own GitHub Action.',
1165
+ },
1166
+ {
1167
+ name: 'Part of existing repository',
1168
+ value: 'existing-repository',
1169
+ description: 'Create ./screenci and add the GitHub Action at the repository root.',
1170
+ },
1271
1171
  ],
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
1172
  });
1280
1173
  }
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
- }
1174
+ function projectNameToDirectoryName(projectName) {
1175
+ return projectName.trim().replace(/\s+/g, '-');
1289
1176
  }
1290
1177
  function getInitProjectRoot() {
1291
1178
  return process.env['SCREENCI_INIT_CWD'] ?? process.cwd();
1292
1179
  }
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
1180
  async function runInitAuth() {
1355
1181
  const appUrl = getDevFrontendUrl();
1356
1182
  try {
@@ -1387,18 +1213,10 @@ async function ensureScreenciSecret() {
1387
1213
  return undefined;
1388
1214
  }
1389
1215
  }
1390
- function checkNodeVersion() {
1391
- const [major] = process.versions.node.split('.').map(Number);
1392
- if (major === undefined || major < 18) {
1393
- logger.error(`Error: Node.js 18 or higher is required (current: v${process.versions.node})`);
1394
- process.exit(1);
1395
- }
1396
- }
1397
1216
  async function runInit(projectNameArg, options) {
1398
1217
  const { verbose, install, yes, skill, ci } = options;
1399
- checkNodeVersion();
1400
- checkContainerRuntimeForInit();
1401
1218
  const initCwd = getInitProjectRoot();
1219
+ const existingRepositoryDetected = existsSync(resolve(initCwd, '.git'));
1402
1220
  let projectName = projectNameArg?.trim();
1403
1221
  if (!projectName) {
1404
1222
  projectName = await promptProjectName();
@@ -1407,25 +1225,28 @@ async function runInit(projectNameArg, options) {
1407
1225
  logger.error('Error: Project name is required');
1408
1226
  process.exit(1);
1409
1227
  }
1410
- const dirName = 'screenci';
1228
+ if (existingRepositoryDetected) {
1229
+ logger.info('Existing repository detected');
1230
+ }
1231
+ const repositoryMode = existingRepositoryDetected
1232
+ ? yes
1233
+ ? 'standalone'
1234
+ : await promptInitRepositoryMode()
1235
+ : 'standalone';
1236
+ const isPartOfExistingRepository = repositoryMode === 'existing-repository';
1237
+ const dirName = isPartOfExistingRepository
1238
+ ? 'screenci'
1239
+ : projectNameToDirectoryName(projectName);
1411
1240
  const projectDir = resolve(initCwd, dirName);
1412
- const githubDir = resolve(initCwd, '.github');
1241
+ const githubRootDir = isPartOfExistingRepository ? initCwd : projectDir;
1242
+ const githubDir = resolve(githubRootDir, '.github');
1413
1243
  const githubWorkflowsDir = resolve(githubDir, 'workflows');
1414
1244
  const githubActionPath = resolve(githubWorkflowsDir, 'screenci.yaml');
1245
+ const githubActionProjectDir = isPartOfExistingRepository ? 'screenci' : '.';
1415
1246
  if (existsSync(projectDir)) {
1416
1247
  logger.error(`Error: Directory "${dirName}" already exists`);
1417
1248
  process.exit(1);
1418
1249
  }
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
1250
  const shouldInstallDependencies = yes
1430
1251
  ? true
1431
1252
  : install
@@ -1456,13 +1277,7 @@ async function runInit(projectNameArg, options) {
1456
1277
  '-y',
1457
1278
  ];
1458
1279
  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
- }
1280
+ const screenciDependency = await readCurrentScreenciVersion();
1466
1281
  await mkdir(resolve(projectDir, 'videos'), { recursive: true });
1467
1282
  if (shouldAddGithubActionCi) {
1468
1283
  if (!existsSync(githubDir)) {
@@ -1472,15 +1287,14 @@ async function runInit(projectNameArg, options) {
1472
1287
  await mkdir(githubWorkflowsDir);
1473
1288
  }
1474
1289
  }
1475
- await writeFile(resolve(projectDir, 'screenci.config.ts'), generateConfig(projectName, initTarget));
1476
- await writeFile(resolve(projectDir, 'package.json'), generatePackageJson(dirName, shouldAddPlaywrightCli, screenciDependency));
1290
+ await writeFile(resolve(projectDir, 'screenci.config.ts'), generateConfig(projectName));
1291
+ await writeFile(resolve(projectDir, 'package.json'), generatePackageJson(shouldAddPlaywrightCli, screenciDependency));
1477
1292
  await writeFile(resolve(projectDir, 'tsconfig.json'), generateTsconfig());
1478
1293
  await writeFile(resolve(projectDir, 'README.md'), generateReadme(projectName));
1479
- await writeFile(resolve(projectDir, 'Dockerfile'), generateDockerfile());
1480
1294
  await writeFile(resolve(projectDir, '.gitignore'), generateGitignore());
1481
1295
  await writeFile(resolve(projectDir, 'videos', 'example.video.ts'), generateExampleVideo());
1482
1296
  if (shouldAddGithubActionCi) {
1483
- await writeFile(githubActionPath, generateGithubAction());
1297
+ await writeFile(githubActionPath, generateGithubAction(githubActionProjectDir));
1484
1298
  }
1485
1299
  await writeFile(resolve(projectDir, '.env'), '');
1486
1300
  logger.info(`Initialized screenci project "${projectName}" in ${projectDir}/`);
@@ -1489,11 +1303,13 @@ async function runInit(projectNameArg, options) {
1489
1303
  logger.info(' package.json');
1490
1304
  logger.info(' tsconfig.json');
1491
1305
  logger.info(' README.md');
1492
- logger.info(' Dockerfile');
1493
1306
  logger.info(' .gitignore');
1494
1307
  logger.info(' videos/example.video.ts');
1495
1308
  if (shouldAddGithubActionCi) {
1496
- logger.info(' .github/workflows/screenci.yaml');
1309
+ const githubActionDisplayPath = isPartOfExistingRepository
1310
+ ? '.github/workflows/screenci.yaml (outside ./screenci, at repository root)'
1311
+ : '.github/workflows/screenci.yaml';
1312
+ logger.info(` ${githubActionDisplayPath}`);
1497
1313
  }
1498
1314
  logger.info(' .env (empty placeholder)');
1499
1315
  logger.info('');
@@ -1514,24 +1330,14 @@ async function runInit(projectNameArg, options) {
1514
1330
  }
1515
1331
  }
1516
1332
  if (verbose) {
1517
- const installArgs = devScreenciPackageRoot
1518
- ? ['install', '--include=dev', '--install-links']
1519
- : ['install', '--include=dev'];
1333
+ const installArgs = ['install', '--include=dev'];
1520
1334
  logger.info(`Running 'npm ${installArgs.join(' ')}'...`);
1521
1335
  await spawnInherited('npm', installArgs, projectDir, 'screenci init');
1522
1336
  }
1523
1337
  else {
1524
1338
  const spinner = ora('Running npm install...').start();
1525
1339
  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];
1340
+ const installArgs = ['install', '--include=dev', '--prefix', projectDir];
1535
1341
  await spawnSilent('npm', installArgs);
1536
1342
  spinner.succeed('npm install complete');
1537
1343
  }
@@ -1575,42 +1381,8 @@ export async function main() {
1575
1381
  .allowUnknownOption(true)
1576
1382
  .action(async () => {
1577
1383
  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')
1384
+ await run('record', parsed.otherArgs, parsed.configPath);
1385
+ if (process.env.SCREENCI_RECORDING === 'true')
1614
1386
  return;
1615
1387
  // After recording, upload results to API if configured
1616
1388
  const resolvedConfigPath = findScreenCIConfig(parsed.configPath);
@@ -1632,6 +1404,7 @@ export async function main() {
1632
1404
  const screenciDir = resolve(configDir, '.screenci');
1633
1405
  let projectId = null;
1634
1406
  try {
1407
+ logger.info('');
1635
1408
  projectId = await uploadRecordings(screenciDir, screenciConfig.projectName, apiUrl, secret);
1636
1409
  }
1637
1410
  catch (err) {
@@ -1659,9 +1432,23 @@ export async function main() {
1659
1432
  .allowUnknownOption(true)
1660
1433
  .action(async () => {
1661
1434
  const parsed = parseConfigCliArgs(getSubcommandArgv('test'));
1435
+ const resolvedConfigPath = findScreenCIConfig(parsed.configPath);
1436
+ if (resolvedConfigPath) {
1437
+ try {
1438
+ const screenciConfig = await loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath);
1439
+ if (screenciConfig.envFile) {
1440
+ const envFilePath = resolve(dirname(resolvedConfigPath), screenciConfig.envFile);
1441
+ loadEnvFile(envFilePath, true);
1442
+ }
1443
+ }
1444
+ catch (err) {
1445
+ logger.warn('Failed to load config for test env:', err);
1446
+ }
1447
+ }
1662
1448
  await run('test', parsed.otherArgs, parsed.configPath);
1663
- if (process.env.SCREENCI_IN_CONTAINER === 'true')
1449
+ if (process.env.SCREENCI_RECORDING === 'true')
1664
1450
  return;
1451
+ logger.info('');
1665
1452
  logger.info(`Tests passed. Run ${pc.cyan('npx screenci record')} to render the videos.`);
1666
1453
  });
1667
1454
  program
@@ -1756,7 +1543,6 @@ function getSubcommandArgv(command) {
1756
1543
  function parseRecordCliArgs(args) {
1757
1544
  let configPath;
1758
1545
  let verbose = false;
1759
- let forcedRuntime;
1760
1546
  const otherArgs = [];
1761
1547
  for (let i = 0; i < args.length; i++) {
1762
1548
  const arg = args[i];
@@ -1774,12 +1560,6 @@ function parseRecordCliArgs(args) {
1774
1560
  else if (arg === '--verbose' || arg === '-v') {
1775
1561
  verbose = true;
1776
1562
  }
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
1563
  else {
1784
1564
  otherArgs.push(arg);
1785
1565
  }
@@ -1787,7 +1567,6 @@ function parseRecordCliArgs(args) {
1787
1567
  return {
1788
1568
  configPath,
1789
1569
  verbose,
1790
- forcedRuntime,
1791
1570
  otherArgs,
1792
1571
  };
1793
1572
  }
@@ -1860,122 +1639,6 @@ function spawnInherited(cmd, args, cwd, activityLabel = cmd) {
1860
1639
  });
1861
1640
  });
1862
1641
  }
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
1642
  async function run(command, additionalArgs, customConfigPath) {
1980
1643
  const configPath = findScreenCIConfig(customConfigPath);
1981
1644
  if (!configPath) {
@@ -1985,6 +1648,10 @@ async function run(command, additionalArgs, customConfigPath) {
1985
1648
  logger.error(errorMsg);
1986
1649
  process.exit(1);
1987
1650
  }
1651
+ if (command === 'test' || process.env.SCREENCI_RECORDING !== 'true') {
1652
+ await loadEnvFileFromConfigSource(configPath, false);
1653
+ }
1654
+ const envForChild = { ...process.env };
1988
1655
  // Only validate args for record command
1989
1656
  if (command === 'record') {
1990
1657
  await ensureScreenciSecret();
@@ -1992,30 +1659,16 @@ async function run(command, additionalArgs, customConfigPath) {
1992
1659
  const screenciDir = resolve(dirname(configPath), '.screenci');
1993
1660
  clearDirectory(screenciDir);
1994
1661
  }
1995
- const mode = command === 'test' ? 'tests' : 'recorder';
1996
- if (process.env.SCREENCI_IN_CONTAINER !== 'true') {
1997
- logger.info(`Running ScreenCI ${mode} with npx...`);
1662
+ if (process.env.SCREENCI_RECORDING !== 'true') {
1998
1663
  logger.info(`Using config: ${configPath}`);
1999
1664
  }
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, {
1665
+ const playwrightArgs = ['test', '--config', configPath, ...additionalArgs];
1666
+ const child = spawn('playwright', playwrightArgs, {
2014
1667
  stdio: 'inherit',
2015
1668
  env: {
2016
- ...buildChildEnv(),
1669
+ ...envForChild,
2017
1670
  // Enable recording only for record command
2018
- ...(command === 'record' ? { SCREENCI_RECORD: 'true' } : {}),
1671
+ ...(command === 'record' ? { SCREENCI_RECORDING: 'true' } : {}),
2019
1672
  },
2020
1673
  });
2021
1674
  const childSignals = forwardChildSignals(child, `screenci ${command}`);
@@ -2048,8 +1701,11 @@ async function run(command, additionalArgs, customConfigPath) {
2048
1701
  // Check if this module is the main module (handles symlinks properly)
2049
1702
  const currentFile = fileURLToPath(import.meta.url);
2050
1703
  const mainFile = process.argv[1] ? realpathSync(process.argv[1]) : null;
1704
+ const currentRealFile = realpathSync(currentFile);
2051
1705
  if (mainFile &&
2052
- (currentFile === mainFile || currentFile === realpathSync(mainFile))) {
1706
+ (currentFile === mainFile ||
1707
+ currentRealFile === mainFile ||
1708
+ currentFile === realpathSync(mainFile))) {
2053
1709
  main().catch((error) => {
2054
1710
  logger.error('Error:', error.message);
2055
1711
  process.exit(1);