screenci 0.0.45 → 0.0.47
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/README.md +5 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +103 -445
- package/dist/cli.js.map +1 -1
- package/dist/docs/manifest.d.ts +26 -18
- package/dist/docs/manifest.d.ts.map +1 -1
- package/dist/docs/manifest.js +14 -8
- package/dist/docs/manifest.js.map +1 -1
- package/dist/docs/video-sources/assets-and-overlays.video.d.ts +2 -0
- package/dist/docs/video-sources/assets-and-overlays.video.d.ts.map +1 -0
- package/dist/docs/video-sources/assets-and-overlays.video.js +40 -0
- package/dist/docs/video-sources/assets-and-overlays.video.js.map +1 -0
- package/dist/docs/video-sources/installation.video.js +1 -3
- package/dist/docs/video-sources/installation.video.js.map +1 -1
- package/dist/docs/videos.d.ts +38 -0
- package/dist/docs/videos.d.ts.map +1 -1
- package/dist/docs/videos.js +17 -0
- package/dist/docs/videos.js.map +1 -1
- package/dist/src/asset.d.ts +10 -9
- package/dist/src/asset.d.ts.map +1 -1
- package/dist/src/asset.js +4 -9
- package/dist/src/asset.js.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +4 -1
- package/dist/src/config.js.map +1 -1
- package/dist/src/cue.d.ts.map +1 -1
- package/dist/src/cue.js +12 -4
- package/dist/src/cue.js.map +1 -1
- package/dist/src/defaults.js +2 -2
- package/dist/src/defaults.js.map +1 -1
- package/dist/src/init.d.ts +12 -0
- package/dist/src/init.d.ts.map +1 -0
- package/dist/src/init.js +673 -0
- package/dist/src/init.js.map +1 -0
- package/dist/src/types.d.ts +16 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/video.d.ts.map +1 -1
- package/dist/src/video.js +3 -2
- package/dist/src/video.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -1
package/dist/cli.js
CHANGED
|
@@ -3,14 +3,13 @@ import { createReadStream } from 'fs';
|
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync } from 'fs';
|
|
4
4
|
import { createHash } from 'crypto';
|
|
5
5
|
import { createServer } from 'http';
|
|
6
|
-
import { appendFile,
|
|
7
|
-
import {
|
|
6
|
+
import { appendFile, readdir, readFile, stat } from 'fs/promises';
|
|
7
|
+
import { delimiter, dirname, relative as pathRelative, resolve } from 'path';
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
9
9
|
import { Command, CommanderError } from 'commander';
|
|
10
|
-
import { input } from '@inquirer/prompts';
|
|
11
|
-
import ora from 'ora';
|
|
12
10
|
import pc from 'picocolors';
|
|
13
11
|
import { logger } from './src/logger.js';
|
|
12
|
+
import { determinePackageManager, parsePackageManager, runInit, } from './src/init.js';
|
|
14
13
|
import { SCREENCI_DISABLE_RECORDING_TIMINGS_ENV, SCREENCI_MOCK_RECORD_ENV, } from './src/runtimeMode.js';
|
|
15
14
|
import { DEFAULT_RECORD_UPLOAD_POLICY } from './src/defaults.js';
|
|
16
15
|
import { findDuplicateTitles, formatDuplicateTitlesMessage, } from './src/titleValidation.js';
|
|
@@ -47,6 +46,11 @@ async function collectDiscoveredTestTitles(configPath, additionalArgs, env) {
|
|
|
47
46
|
const child = spawn(spawnSpec.command, spawnSpec.args, {
|
|
48
47
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
49
48
|
...(spawnSpec.shell !== undefined ? { shell: spawnSpec.shell } : {}),
|
|
49
|
+
...(spawnSpec.windowsVerbatimArguments !== undefined
|
|
50
|
+
? {
|
|
51
|
+
windowsVerbatimArguments: spawnSpec.windowsVerbatimArguments,
|
|
52
|
+
}
|
|
53
|
+
: {}),
|
|
50
54
|
env,
|
|
51
55
|
});
|
|
52
56
|
const childSignals = forwardChildSignals(child, 'screenci title validation', {
|
|
@@ -189,6 +193,9 @@ function createUploadProgressReporter(videoNames, verbose) {
|
|
|
189
193
|
complete(index, status) {
|
|
190
194
|
logger.info(formatUploadProgressLine(videoNames[index] ?? 'unknown', status));
|
|
191
195
|
},
|
|
196
|
+
info(message) {
|
|
197
|
+
logger.info(message);
|
|
198
|
+
},
|
|
192
199
|
};
|
|
193
200
|
}
|
|
194
201
|
const statuses = new Array(videoNames.length);
|
|
@@ -198,12 +205,23 @@ function createUploadProgressReporter(videoNames, verbose) {
|
|
|
198
205
|
process.stdout.write(`${hasRendered && videoNames.length > 0 ? `\u001B[${videoNames.length}A` : ''}${renderedLines.map((line) => `\r\u001B[2K${line}`).join('\n')}\n`);
|
|
199
206
|
hasRendered = true;
|
|
200
207
|
};
|
|
208
|
+
const clear = () => {
|
|
209
|
+
if (!hasRendered || videoNames.length === 0)
|
|
210
|
+
return;
|
|
211
|
+
process.stdout.write(`\u001B[${videoNames.length}A${Array.from({ length: videoNames.length }, () => '\r\u001B[2K').join('\n')}\n`);
|
|
212
|
+
hasRendered = false;
|
|
213
|
+
};
|
|
201
214
|
render();
|
|
202
215
|
return {
|
|
203
216
|
complete(index, status) {
|
|
204
217
|
statuses[index] = status;
|
|
205
218
|
render();
|
|
206
219
|
},
|
|
220
|
+
info(message) {
|
|
221
|
+
clear();
|
|
222
|
+
logger.info(message);
|
|
223
|
+
render();
|
|
224
|
+
},
|
|
207
225
|
};
|
|
208
226
|
}
|
|
209
227
|
async function loadUploadCandidate(screenciDir, entry, verbose) {
|
|
@@ -277,7 +295,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
277
295
|
logger.info(`recordingId=${recordingId} projectId=${projectId}`);
|
|
278
296
|
logger.info(`assets=${preparedUploadAssets.length} recordingHash=${recordingHash ?? 'none'}`);
|
|
279
297
|
}
|
|
280
|
-
await uploadAssets(preparedUploadAssets, apiUrl, secret, recordingId, uploadAbort.signal, uploadAbort.throwIfAborted);
|
|
298
|
+
await uploadAssets(preparedUploadAssets, apiUrl, secret, recordingId, uploadAbort.signal, uploadAbort.throwIfAborted, progressReporter);
|
|
281
299
|
if (existsSync(recordingPath)) {
|
|
282
300
|
uploadAbort.throwIfAborted();
|
|
283
301
|
const fileStat = await stat(recordingPath);
|
|
@@ -387,59 +405,53 @@ function createUploadAbortController(activityLabel) {
|
|
|
387
405
|
cleanup,
|
|
388
406
|
};
|
|
389
407
|
}
|
|
390
|
-
function quoteWindowsCommandArg(arg) {
|
|
391
|
-
if (arg.length === 0)
|
|
392
|
-
return '""';
|
|
393
|
-
if (/^[A-Za-z0-9_./:\\=-]+$/.test(arg))
|
|
394
|
-
return arg;
|
|
395
|
-
return `"${arg.replace(/"/g, '""')}"`;
|
|
396
|
-
}
|
|
397
408
|
function resolveSpawnSpec(cmd, args) {
|
|
398
409
|
if (process.platform !== 'win32') {
|
|
399
410
|
return { command: cmd, args };
|
|
400
411
|
}
|
|
401
|
-
const
|
|
402
|
-
if (!
|
|
412
|
+
const windowsCmdShims = new Set(['npm', 'npx', 'playwright', 'pnpm']);
|
|
413
|
+
if (!windowsCmdShims.has(cmd)) {
|
|
403
414
|
return { command: cmd, args };
|
|
404
415
|
}
|
|
405
|
-
const commandLine = [cmd, ...args].map(quoteWindowsCommandArg).join(' ');
|
|
406
416
|
return {
|
|
407
|
-
command: 'cmd',
|
|
408
|
-
args: ['/d', '/c',
|
|
417
|
+
command: process.env.comspec ?? 'cmd.exe',
|
|
418
|
+
args: ['/d', '/s', '/c', `"${buildWindowsBatchCommandLine(cmd, args)}"`],
|
|
419
|
+
windowsVerbatimArguments: true,
|
|
409
420
|
};
|
|
410
421
|
}
|
|
411
|
-
function
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
}
|
|
422
|
+
function quoteWindowsBatchArg(arg) {
|
|
423
|
+
if (arg.length === 0) {
|
|
424
|
+
return '""';
|
|
425
|
+
}
|
|
426
|
+
return `"${arg
|
|
427
|
+
.replace(/(\\*)"/g, '$1$1\\"')
|
|
428
|
+
.replace(/(\\+)$/g, '$1$1')
|
|
429
|
+
.replace(/%/g, '%%')}"`;
|
|
430
|
+
}
|
|
431
|
+
function buildWindowsBatchCommandLine(cmd, args) {
|
|
432
|
+
return [resolveWindowsCmdShim(cmd), ...args]
|
|
433
|
+
.map(quoteWindowsBatchArg)
|
|
434
|
+
.join(' ');
|
|
435
|
+
}
|
|
436
|
+
function resolveWindowsCmdShim(cmd) {
|
|
437
|
+
const shimName = `${cmd}.cmd`;
|
|
438
|
+
const pathEntries = process.env.PATH?.split(delimiter) ?? [];
|
|
439
|
+
for (const entry of pathEntries) {
|
|
440
|
+
if (!entry)
|
|
441
|
+
continue;
|
|
442
|
+
const shimPath = resolve(entry, shimName);
|
|
443
|
+
if (existsSync(shimPath)) {
|
|
444
|
+
return shimPath;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const bundledShimCommands = new Set(['npm', 'npx']);
|
|
448
|
+
if (bundledShimCommands.has(cmd)) {
|
|
449
|
+
const bundledShimPath = resolve(dirname(process.execPath), shimName);
|
|
450
|
+
if (existsSync(bundledShimPath)) {
|
|
451
|
+
return bundledShimPath;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return shimName;
|
|
443
455
|
}
|
|
444
456
|
function forwardChildSignals(child, activityLabel, options = {}) {
|
|
445
457
|
let forwardedSignal = null;
|
|
@@ -764,6 +776,16 @@ export function formatUploadStartFailureMessage(videoName, status, responseText,
|
|
|
764
776
|
}
|
|
765
777
|
return `Failed to start upload for "${videoName}": ${status}${hint401(status, secret)}`;
|
|
766
778
|
}
|
|
779
|
+
const EXPRESSIVE_TIER_ERROR_PREFIX = 'Expressive narration and style prompts require the Business tier.';
|
|
780
|
+
export function formatFailedVideoMessage(videoName, message) {
|
|
781
|
+
if (message.startsWith(EXPRESSIVE_TIER_ERROR_PREFIX)) {
|
|
782
|
+
return [
|
|
783
|
+
`${videoName}: ${message}`,
|
|
784
|
+
"If you want to keep using the current tier, remove `voice.style` or `modelType: 'expressive'` from `createNarration()`.",
|
|
785
|
+
].join('\n');
|
|
786
|
+
}
|
|
787
|
+
return `${videoName}: ${message}`;
|
|
788
|
+
}
|
|
767
789
|
export function printUploadStartFailureMessage(videoName, status, responseText, secret) {
|
|
768
790
|
const message = formatUploadStartFailureMessage(videoName, status, responseText, secret);
|
|
769
791
|
if (responseText.trim().length > 0) {
|
|
@@ -772,7 +794,15 @@ export function printUploadStartFailureMessage(videoName, status, responseText,
|
|
|
772
794
|
}
|
|
773
795
|
logger.warn(message);
|
|
774
796
|
}
|
|
775
|
-
async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIfAborted) {
|
|
797
|
+
async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIfAborted, progressReporter) {
|
|
798
|
+
const logInfo = (message) => {
|
|
799
|
+
if (progressReporter) {
|
|
800
|
+
progressReporter.info(message);
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
logger.info(message);
|
|
804
|
+
}
|
|
805
|
+
};
|
|
776
806
|
for (const asset of assets) {
|
|
777
807
|
throwIfAborted();
|
|
778
808
|
try {
|
|
@@ -798,7 +828,7 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
798
828
|
}
|
|
799
829
|
const checkBody = (await checkRes.json());
|
|
800
830
|
if (checkBody.exists) {
|
|
801
|
-
|
|
831
|
+
logInfo(`Asset already exists: ${asset.path}`);
|
|
802
832
|
continue;
|
|
803
833
|
}
|
|
804
834
|
if (!asset.fileBuffer || !asset.contentType) {
|
|
@@ -825,14 +855,14 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
825
855
|
if (!res.ok) {
|
|
826
856
|
const text = await res.text();
|
|
827
857
|
if (res.status === 409 && text.includes('already exists')) {
|
|
828
|
-
|
|
858
|
+
logInfo(`Asset already exists: ${asset.path}`);
|
|
829
859
|
}
|
|
830
860
|
else {
|
|
831
861
|
logger.warn(`Failed to upload asset ${asset.path}: ${res.status} ${text}${hint401(res.status, secret)}`);
|
|
832
862
|
}
|
|
833
863
|
}
|
|
834
864
|
else {
|
|
835
|
-
|
|
865
|
+
logInfo(`Asset uploaded: ${asset.path}`);
|
|
836
866
|
}
|
|
837
867
|
}
|
|
838
868
|
catch (err) {
|
|
@@ -1011,6 +1041,12 @@ export function extractRecordUploadPolicyLiteral(configSource) {
|
|
|
1011
1041
|
const templateLiteralMatch = configSource.match(/record\s*:\s*\{[\s\S]*?upload\s*:\s*`(passed-only|all-or-nothing)`/);
|
|
1012
1042
|
return templateLiteralMatch?.[1];
|
|
1013
1043
|
}
|
|
1044
|
+
export function extractMockRecordLiteral(configSource) {
|
|
1045
|
+
const match = configSource.match(/test\s*:\s*\{[\s\S]*?mockRecord\s*:\s*(true|false)/);
|
|
1046
|
+
if (!match)
|
|
1047
|
+
return undefined;
|
|
1048
|
+
return match[1] === 'true';
|
|
1049
|
+
}
|
|
1014
1050
|
function resolveRecordUploadPolicy(config) {
|
|
1015
1051
|
return config.record?.upload ?? DEFAULT_RECORD_UPLOAD_POLICY;
|
|
1016
1052
|
}
|
|
@@ -1022,10 +1058,12 @@ async function tryReadConfigFromSource(resolvedConfigPath) {
|
|
|
1022
1058
|
}
|
|
1023
1059
|
const envFile = extractConfigStringLiteral(configSource, 'envFile');
|
|
1024
1060
|
const recordUpload = extractRecordUploadPolicyLiteral(configSource);
|
|
1061
|
+
const mockRecord = extractMockRecordLiteral(configSource);
|
|
1025
1062
|
return {
|
|
1026
1063
|
projectName,
|
|
1027
1064
|
...(envFile !== undefined ? { envFile } : {}),
|
|
1028
1065
|
...(recordUpload !== undefined ? { record: { upload: recordUpload } } : {}),
|
|
1066
|
+
...(mockRecord !== undefined ? { test: { mockRecord } } : {}),
|
|
1029
1067
|
};
|
|
1030
1068
|
}
|
|
1031
1069
|
export function getConfigModuleSpecifier(resolvedConfigPath) {
|
|
@@ -1103,151 +1141,6 @@ async function updateVideoVisibility(videoId, isPublic, configPath) {
|
|
|
1103
1141
|
}
|
|
1104
1142
|
logger.info(`${isPublic ? 'Made public' : 'Made private'}: ${videoId}`);
|
|
1105
1143
|
}
|
|
1106
|
-
function generateConfig(projectName) {
|
|
1107
|
-
return `import { defineConfig } from 'screenci'
|
|
1108
|
-
|
|
1109
|
-
export default defineConfig({
|
|
1110
|
-
projectName: ${JSON.stringify(projectName)},
|
|
1111
|
-
envFile: '.env',
|
|
1112
|
-
videoDir: './videos',
|
|
1113
|
-
use: {
|
|
1114
|
-
recordOptions: {
|
|
1115
|
-
aspectRatio: '16:9',
|
|
1116
|
-
quality: '1080p',
|
|
1117
|
-
fps: 60,
|
|
1118
|
-
},
|
|
1119
|
-
},
|
|
1120
|
-
projects: [
|
|
1121
|
-
{
|
|
1122
|
-
name: 'chromium',
|
|
1123
|
-
},
|
|
1124
|
-
],
|
|
1125
|
-
})
|
|
1126
|
-
`;
|
|
1127
|
-
}
|
|
1128
|
-
function generatePackageJson(includePlaywrightCli = false, screenciDependency = 'latest') {
|
|
1129
|
-
const devDependencies = {};
|
|
1130
|
-
if (includePlaywrightCli) {
|
|
1131
|
-
devDependencies['@playwright/cli'] = 'latest';
|
|
1132
|
-
}
|
|
1133
|
-
return (JSON.stringify({
|
|
1134
|
-
type: 'module',
|
|
1135
|
-
dependencies: {
|
|
1136
|
-
screenci: screenciDependency,
|
|
1137
|
-
'@playwright/test': '^1.59.0',
|
|
1138
|
-
},
|
|
1139
|
-
devDependencies,
|
|
1140
|
-
}, null, 2) + '\n');
|
|
1141
|
-
}
|
|
1142
|
-
async function readCurrentScreenciVersion() {
|
|
1143
|
-
const currentFileDir = dirname(fileURLToPath(import.meta.url));
|
|
1144
|
-
const packageJsonPaths = [
|
|
1145
|
-
resolve(currentFileDir, 'package.json'),
|
|
1146
|
-
resolve(currentFileDir, '../package.json'),
|
|
1147
|
-
];
|
|
1148
|
-
for (const packageJsonPath of packageJsonPaths) {
|
|
1149
|
-
try {
|
|
1150
|
-
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
|
|
1151
|
-
if (typeof packageJson.version === 'string') {
|
|
1152
|
-
return packageJson.version;
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
catch {
|
|
1156
|
-
// Try the next candidate path.
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
return 'latest';
|
|
1160
|
-
}
|
|
1161
|
-
function generateReadme(projectName) {
|
|
1162
|
-
return `# ${projectName}
|
|
1163
|
-
|
|
1164
|
-
This project uses ScreenCI + Playwright to create and upload polished product videos.
|
|
1165
|
-
|
|
1166
|
-
## How video recording works
|
|
1167
|
-
|
|
1168
|
-
Write video scripts in \`videos/*.video.ts\` and use \`video(...)\` calls to create product videos. These are very similar to Playwright \`.test.ts\` and \`test(...)\` calls.
|
|
1169
|
-
|
|
1170
|
-
Learn more: https://screenci.com/docs
|
|
1171
|
-
|
|
1172
|
-
## Quick start
|
|
1173
|
-
|
|
1174
|
-
1. Create your own videos in \`videos/*.video.ts\`, either manually or with an AI agent using your source code or a URL.
|
|
1175
|
-
|
|
1176
|
-
2. Run videos locally to test the script working:
|
|
1177
|
-
|
|
1178
|
-
\`npx screenci test\` or with UI mode: \`npx screenci test --ui\`
|
|
1179
|
-
|
|
1180
|
-
3. Record videos:
|
|
1181
|
-
|
|
1182
|
-
\`npx screenci record\`
|
|
1183
|
-
|
|
1184
|
-
4. View results on screenci.com and optionally enable a public URL to embed the video on your site.
|
|
1185
|
-
`;
|
|
1186
|
-
}
|
|
1187
|
-
function generateGitignore() {
|
|
1188
|
-
return `/playwright-report/
|
|
1189
|
-
.screenci
|
|
1190
|
-
.playwright-cli/
|
|
1191
|
-
node_modules/
|
|
1192
|
-
.env
|
|
1193
|
-
`;
|
|
1194
|
-
}
|
|
1195
|
-
function generateGithubAction() {
|
|
1196
|
-
return `name: ScreenCI
|
|
1197
|
-
|
|
1198
|
-
on:
|
|
1199
|
-
push:
|
|
1200
|
-
branches: [main]
|
|
1201
|
-
workflow_dispatch:
|
|
1202
|
-
|
|
1203
|
-
jobs:
|
|
1204
|
-
record:
|
|
1205
|
-
runs-on: ubuntu-latest
|
|
1206
|
-
environment:
|
|
1207
|
-
name: screenci
|
|
1208
|
-
url: \${{ steps.record.outputs.screenci_project_url }}
|
|
1209
|
-
steps:
|
|
1210
|
-
- name: Check SCREENCI_SECRET
|
|
1211
|
-
env:
|
|
1212
|
-
SCREENCI_SECRET: \${{ secrets.SCREENCI_SECRET }}
|
|
1213
|
-
run: |
|
|
1214
|
-
if [ -z "$SCREENCI_SECRET" ]; then
|
|
1215
|
-
echo "::error::SCREENCI_SECRET is not set. Copy it from https://app.screenci.com/secrets or ./.env, add it under Settings → Secrets and variables → Actions → Repository secrets, and then rerun this action."
|
|
1216
|
-
exit 1
|
|
1217
|
-
fi
|
|
1218
|
-
|
|
1219
|
-
- uses: actions/checkout@v4
|
|
1220
|
-
|
|
1221
|
-
- uses: actions/setup-node@v4
|
|
1222
|
-
with:
|
|
1223
|
-
node-version: 24
|
|
1224
|
-
cache: npm
|
|
1225
|
-
cache-dependency-path: package-lock.json
|
|
1226
|
-
|
|
1227
|
-
- name: Install dependencies
|
|
1228
|
-
working-directory: .
|
|
1229
|
-
run: npm ci
|
|
1230
|
-
|
|
1231
|
-
- name: Cache Playwright Chromium
|
|
1232
|
-
uses: actions/cache@v5
|
|
1233
|
-
id: pw-cache
|
|
1234
|
-
with:
|
|
1235
|
-
path: ~/.cache/ms-playwright
|
|
1236
|
-
key: playwright-\${{ runner.os }}-\${{ hashFiles('package-lock.json') }}
|
|
1237
|
-
|
|
1238
|
-
- name: Install Chromium
|
|
1239
|
-
if: steps.pw-cache.outputs.cache-hit != 'true'
|
|
1240
|
-
working-directory: .
|
|
1241
|
-
run: npx playwright install chromium
|
|
1242
|
-
|
|
1243
|
-
- id: record
|
|
1244
|
-
name: Record
|
|
1245
|
-
working-directory: .
|
|
1246
|
-
env:
|
|
1247
|
-
SCREENCI_SECRET: \${{ secrets.SCREENCI_SECRET }}
|
|
1248
|
-
run: npx screenci record
|
|
1249
|
-
`;
|
|
1250
|
-
}
|
|
1251
1144
|
function openBrowser(url) {
|
|
1252
1145
|
try {
|
|
1253
1146
|
if (process.platform === 'win32') {
|
|
@@ -1307,91 +1200,6 @@ async function performBrowserLogin(appUrl) {
|
|
|
1307
1200
|
server.on('close', () => clearTimeout(timeout));
|
|
1308
1201
|
});
|
|
1309
1202
|
}
|
|
1310
|
-
function generateExampleVideo() {
|
|
1311
|
-
return `import { autoZoom, createNarration, hide, video, voices } from 'screenci'
|
|
1312
|
-
|
|
1313
|
-
const narration = createNarration({
|
|
1314
|
-
voice: { name: voices.Sophie },
|
|
1315
|
-
languages: {
|
|
1316
|
-
en: {
|
|
1317
|
-
cues: {
|
|
1318
|
-
intro:
|
|
1319
|
-
'This video shows how to get started with ScreenCI [pronounce: screen see eye].',
|
|
1320
|
-
docs: 'You can find the documentation linked right on the front page.',
|
|
1321
|
-
},
|
|
1322
|
-
},
|
|
1323
|
-
},
|
|
1324
|
-
})
|
|
1325
|
-
|
|
1326
|
-
video('How to get started', async ({ page }) => {
|
|
1327
|
-
await hide(async () => {
|
|
1328
|
-
await page.goto('https://screenci.com')
|
|
1329
|
-
await page.getByText('ScreenCI').first().waitFor()
|
|
1330
|
-
})
|
|
1331
|
-
|
|
1332
|
-
await narration.intro()
|
|
1333
|
-
await narration.docs()
|
|
1334
|
-
|
|
1335
|
-
await autoZoom(async () => {
|
|
1336
|
-
await page.getByRole('link', { name: 'View Documentation' }).click()
|
|
1337
|
-
})
|
|
1338
|
-
|
|
1339
|
-
await page.getByRole('heading', { level: 1, name: 'Installation' }).first().waitFor()
|
|
1340
|
-
})
|
|
1341
|
-
`;
|
|
1342
|
-
}
|
|
1343
|
-
function getDefaultInitProjectName() {
|
|
1344
|
-
const directoryName = basename(getInitProjectRoot());
|
|
1345
|
-
return directoryName.length > 0 ? directoryName : 'screenci-project';
|
|
1346
|
-
}
|
|
1347
|
-
async function promptProjectName() {
|
|
1348
|
-
return input({
|
|
1349
|
-
message: 'Project name:',
|
|
1350
|
-
default: getDefaultInitProjectName(),
|
|
1351
|
-
});
|
|
1352
|
-
}
|
|
1353
|
-
async function promptYesNo(message, defaultValue) {
|
|
1354
|
-
const answer = await input({
|
|
1355
|
-
message,
|
|
1356
|
-
default: defaultValue ? 'y' : 'n',
|
|
1357
|
-
validate: (value) => {
|
|
1358
|
-
const normalized = value.trim().toLowerCase();
|
|
1359
|
-
if (normalized === '' ||
|
|
1360
|
-
normalized === 'y' ||
|
|
1361
|
-
normalized === 'yes' ||
|
|
1362
|
-
normalized === 'n' ||
|
|
1363
|
-
normalized === 'no') {
|
|
1364
|
-
return true;
|
|
1365
|
-
}
|
|
1366
|
-
return 'Enter y or n';
|
|
1367
|
-
},
|
|
1368
|
-
});
|
|
1369
|
-
const normalized = answer.trim().toLowerCase();
|
|
1370
|
-
if (normalized === '')
|
|
1371
|
-
return defaultValue;
|
|
1372
|
-
return normalized === 'y' || normalized === 'yes';
|
|
1373
|
-
}
|
|
1374
|
-
async function promptInitGithubActionWorkflow() {
|
|
1375
|
-
return promptYesNo('Add a GitHub Actions workflow? (Y/n)', true);
|
|
1376
|
-
}
|
|
1377
|
-
async function promptInitPlaywrightBrowsers() {
|
|
1378
|
-
return promptYesNo("Install Playwright browsers (can be done manually via 'npx playwright install chromium')? (Y/n)", true);
|
|
1379
|
-
}
|
|
1380
|
-
async function promptInitPlaywrightOsDependencies() {
|
|
1381
|
-
return promptYesNo("Install Playwright operating system dependencies (might require sudo / root and can be done manually via 'npx playwright install-deps chromium')? (y/N)", false);
|
|
1382
|
-
}
|
|
1383
|
-
async function promptInitScreenCISkill() {
|
|
1384
|
-
return promptYesNo("Install the ScreenCI skill for AI agents (can be done manually via 'npx -y skills add screenci/screenci --skill screenci -y')? (Y/n)", true);
|
|
1385
|
-
}
|
|
1386
|
-
async function promptInitPlaywrightCliSkill() {
|
|
1387
|
-
return promptYesNo("Install playwright-cli for URL-based browser inspection (can be done manually via 'npx -y skills add screenci/screenci --skill playwright-cli -y && npm install @playwright/cli')? (Y/n)", true);
|
|
1388
|
-
}
|
|
1389
|
-
function getInitProjectRoot() {
|
|
1390
|
-
return process.env['SCREENCI_INIT_CWD'] ?? process.cwd();
|
|
1391
|
-
}
|
|
1392
|
-
function getInitScreenciDependencyOverride() {
|
|
1393
|
-
return process.env['SCREENCI_INIT_SCREENCI_DEPENDENCY'];
|
|
1394
|
-
}
|
|
1395
1203
|
export async function ensureScreenciSecret(resolvedConfigPath) {
|
|
1396
1204
|
const existingSecret = process.env.SCREENCI_SECRET;
|
|
1397
1205
|
if (existingSecret)
|
|
@@ -1416,133 +1224,6 @@ export async function ensureScreenciSecret(resolvedConfigPath) {
|
|
|
1416
1224
|
return undefined;
|
|
1417
1225
|
}
|
|
1418
1226
|
}
|
|
1419
|
-
async function runInit(projectNameArg, options) {
|
|
1420
|
-
const { verbose, yes, agent } = options;
|
|
1421
|
-
const initCwd = getInitProjectRoot();
|
|
1422
|
-
let projectName = projectNameArg?.trim();
|
|
1423
|
-
if (!projectName) {
|
|
1424
|
-
projectName = yes ? getDefaultInitProjectName() : await promptProjectName();
|
|
1425
|
-
}
|
|
1426
|
-
if (!projectName) {
|
|
1427
|
-
logger.error('Error: Project name is required');
|
|
1428
|
-
process.exit(1);
|
|
1429
|
-
}
|
|
1430
|
-
const projectDir = initCwd;
|
|
1431
|
-
const githubWorkflowsDir = resolve(projectDir, '.github', 'workflows');
|
|
1432
|
-
const githubActionPath = resolve(githubWorkflowsDir, 'screenci.yaml');
|
|
1433
|
-
const shouldAddGithubActionWorkflow = yes
|
|
1434
|
-
? true
|
|
1435
|
-
: await promptInitGithubActionWorkflow();
|
|
1436
|
-
const shouldInstallPlaywrightBrowsers = yes
|
|
1437
|
-
? true
|
|
1438
|
-
: await promptInitPlaywrightBrowsers();
|
|
1439
|
-
const shouldInstallPlaywrightOsDependencies = yes
|
|
1440
|
-
? false
|
|
1441
|
-
: await promptInitPlaywrightOsDependencies();
|
|
1442
|
-
const shouldInstallScreenCISkill = yes
|
|
1443
|
-
? true
|
|
1444
|
-
: await promptInitScreenCISkill();
|
|
1445
|
-
const shouldInstallPlaywrightCli = yes
|
|
1446
|
-
? true
|
|
1447
|
-
: await promptInitPlaywrightCliSkill();
|
|
1448
|
-
if (shouldAddGithubActionWorkflow && existsSync(githubActionPath)) {
|
|
1449
|
-
logger.error('Error: GitHub Actions workflow ".github/workflows/screenci.yaml" already exists');
|
|
1450
|
-
process.exit(1);
|
|
1451
|
-
}
|
|
1452
|
-
const skills = [];
|
|
1453
|
-
if (shouldInstallScreenCISkill) {
|
|
1454
|
-
skills.push('screenci');
|
|
1455
|
-
}
|
|
1456
|
-
if (shouldInstallPlaywrightCli) {
|
|
1457
|
-
skills.push('playwright-cli');
|
|
1458
|
-
}
|
|
1459
|
-
const skillsArgs = skills.length === 0
|
|
1460
|
-
? null
|
|
1461
|
-
: [
|
|
1462
|
-
'-y',
|
|
1463
|
-
'skills',
|
|
1464
|
-
'add',
|
|
1465
|
-
'screenci/screenci',
|
|
1466
|
-
...(agent ? ['--agent', agent] : []),
|
|
1467
|
-
...skills.flatMap((skillName) => ['--skill', skillName]),
|
|
1468
|
-
'-y',
|
|
1469
|
-
];
|
|
1470
|
-
const skillsCommand = skillsArgs === null ? null : `npx ${skillsArgs.join(' ')}`;
|
|
1471
|
-
const screenciDependency = getInitScreenciDependencyOverride() ?? (await readCurrentScreenciVersion());
|
|
1472
|
-
logger.info("Initializing project in '.'");
|
|
1473
|
-
await mkdir(resolve(projectDir, 'videos'), { recursive: true });
|
|
1474
|
-
if (shouldAddGithubActionWorkflow) {
|
|
1475
|
-
await mkdir(githubWorkflowsDir, { recursive: true });
|
|
1476
|
-
}
|
|
1477
|
-
await writeFile(resolve(projectDir, 'screenci.config.ts'), generateConfig(projectName));
|
|
1478
|
-
await writeFile(resolve(projectDir, 'package.json'), generatePackageJson(shouldInstallPlaywrightCli, screenciDependency));
|
|
1479
|
-
await writeFile(resolve(projectDir, 'README.md'), generateReadme(projectName));
|
|
1480
|
-
await writeFile(resolve(projectDir, '.gitignore'), generateGitignore());
|
|
1481
|
-
await writeFile(resolve(projectDir, 'videos', 'example.video.ts'), generateExampleVideo());
|
|
1482
|
-
if (shouldAddGithubActionWorkflow) {
|
|
1483
|
-
await writeFile(githubActionPath, generateGithubAction());
|
|
1484
|
-
}
|
|
1485
|
-
logger.info(`Initialized screenci project "${projectName}" in .`);
|
|
1486
|
-
logger.info('Files created:');
|
|
1487
|
-
logger.info(' screenci.config.ts');
|
|
1488
|
-
logger.info(' package.json');
|
|
1489
|
-
logger.info(' README.md');
|
|
1490
|
-
logger.info(' .gitignore');
|
|
1491
|
-
logger.info(' videos/example.video.ts');
|
|
1492
|
-
if (shouldAddGithubActionWorkflow) {
|
|
1493
|
-
logger.info(' .github/workflows/screenci.yaml');
|
|
1494
|
-
}
|
|
1495
|
-
logger.info('');
|
|
1496
|
-
if (skillsArgs !== null) {
|
|
1497
|
-
if (verbose) {
|
|
1498
|
-
logger.info(`Running '${skillsCommand}'...`);
|
|
1499
|
-
await spawnInherited('npx', skillsArgs, projectDir, 'screenci init');
|
|
1500
|
-
}
|
|
1501
|
-
else {
|
|
1502
|
-
const spinner = ora('Adding selected AI skills...').start();
|
|
1503
|
-
try {
|
|
1504
|
-
await spawnSilent('npx', skillsArgs, projectDir);
|
|
1505
|
-
spinner.succeed('Selected AI skills added');
|
|
1506
|
-
}
|
|
1507
|
-
catch (err) {
|
|
1508
|
-
spinner.fail('AI skills install failed');
|
|
1509
|
-
throw err;
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
const installArgs = ['install', '--include=dev'];
|
|
1514
|
-
if (verbose) {
|
|
1515
|
-
logger.info(`Running 'npm ${installArgs.join(' ')}'...`);
|
|
1516
|
-
await spawnInherited('npm', installArgs, projectDir, 'screenci init');
|
|
1517
|
-
}
|
|
1518
|
-
else {
|
|
1519
|
-
const spinner = ora('Running npm install...').start();
|
|
1520
|
-
try {
|
|
1521
|
-
await spawnSilent('npm', installArgs, projectDir);
|
|
1522
|
-
spinner.succeed('npm install complete');
|
|
1523
|
-
}
|
|
1524
|
-
catch (err) {
|
|
1525
|
-
spinner.fail('npm install failed');
|
|
1526
|
-
throw err;
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
if (shouldInstallPlaywrightBrowsers) {
|
|
1530
|
-
logger.info("Installing Playwright Chromium with 'npx playwright install chromium'...");
|
|
1531
|
-
await spawnInherited('npx', ['playwright', 'install', 'chromium'], projectDir, 'screenci init');
|
|
1532
|
-
logger.info(`${pc.green('✔')} Playwright Chromium installed successfully`);
|
|
1533
|
-
}
|
|
1534
|
-
if (shouldInstallPlaywrightOsDependencies) {
|
|
1535
|
-
logger.info("Installing Playwright operating system dependencies with 'npx playwright install-deps chromium'...");
|
|
1536
|
-
await spawnInherited('npx', ['playwright', 'install-deps', 'chromium'], projectDir, 'screenci init');
|
|
1537
|
-
logger.info(`${pc.green('✔')} Playwright operating system dependencies installed successfully`);
|
|
1538
|
-
}
|
|
1539
|
-
logger.info('');
|
|
1540
|
-
logger.info('Next steps:');
|
|
1541
|
-
logger.info(' Read README.md for setup and recording flow');
|
|
1542
|
-
logger.info(' Docs: https://screenci.com/docs');
|
|
1543
|
-
logger.info(' npx screenci test');
|
|
1544
|
-
logger.info(' npx screenci record');
|
|
1545
|
-
}
|
|
1546
1227
|
export async function main() {
|
|
1547
1228
|
if (process.argv.length <= 2) {
|
|
1548
1229
|
logger.error('Error: No command provided');
|
|
@@ -1550,6 +1231,7 @@ export async function main() {
|
|
|
1550
1231
|
process.exit(1);
|
|
1551
1232
|
}
|
|
1552
1233
|
const program = new Command();
|
|
1234
|
+
const defaultPackageManager = determinePackageManager();
|
|
1553
1235
|
program.name('screenci');
|
|
1554
1236
|
program.exitOverride();
|
|
1555
1237
|
// record command — playwright args pass through as-is
|
|
@@ -1634,7 +1316,7 @@ export async function main() {
|
|
|
1634
1316
|
}
|
|
1635
1317
|
if (hadFailures) {
|
|
1636
1318
|
for (const failedVideo of failedVideoMessages) {
|
|
1637
|
-
logger.warn(
|
|
1319
|
+
logger.warn(formatFailedVideoMessage(failedVideo.videoName, failedVideo.message));
|
|
1638
1320
|
}
|
|
1639
1321
|
logger.warn(`Not all recordings succeeded to upload. Failed videos: ${failedVideoNames.join(', ') || 'unknown'}. Some videos may be missing from the project.`);
|
|
1640
1322
|
if (playwrightFailure === null) {
|
|
@@ -1662,10 +1344,12 @@ export async function main() {
|
|
|
1662
1344
|
.allowUnknownOption(true)
|
|
1663
1345
|
.action(async () => {
|
|
1664
1346
|
const parsed = parseConfigCliArgs(getSubcommandArgv('test'));
|
|
1347
|
+
let configMockRecord = false;
|
|
1665
1348
|
const resolvedConfigPath = findScreenCIConfig(parsed.configPath);
|
|
1666
1349
|
if (resolvedConfigPath) {
|
|
1667
1350
|
try {
|
|
1668
1351
|
const screenciConfig = await loadRecordConfigWithoutPlaywrightCollision(resolvedConfigPath);
|
|
1352
|
+
configMockRecord = screenciConfig.test?.mockRecord ?? false;
|
|
1669
1353
|
if (screenciConfig.envFile) {
|
|
1670
1354
|
const envFilePath = resolve(dirname(resolvedConfigPath), screenciConfig.envFile);
|
|
1671
1355
|
loadEnvFile(envFilePath, true);
|
|
@@ -1675,7 +1359,7 @@ export async function main() {
|
|
|
1675
1359
|
logger.warn('Failed to load config for test env:', err);
|
|
1676
1360
|
}
|
|
1677
1361
|
}
|
|
1678
|
-
await run('test', parsed.otherArgs, parsed.configPath, parsed.verbose, parsed.mockRecord);
|
|
1362
|
+
await run('test', parsed.otherArgs, parsed.configPath, parsed.verbose, parsed.mockRecord || configMockRecord);
|
|
1679
1363
|
if (process.env.SCREENCI_RECORDING === 'true')
|
|
1680
1364
|
return;
|
|
1681
1365
|
logger.info('');
|
|
@@ -1707,6 +1391,7 @@ export async function main() {
|
|
|
1707
1391
|
.command('init [name]')
|
|
1708
1392
|
.description('Initialize a new screenci project')
|
|
1709
1393
|
.option('--agent <name>', 'target agent for skills install, e.g. opencode. Supported agents: https://github.com/vercel-labs/skills#supported-agents')
|
|
1394
|
+
.option('--package-manager <manager>', `package manager to use: npm or pnpm (default: ${defaultPackageManager})`)
|
|
1710
1395
|
.option('-y, --yes', 'accept init defaults')
|
|
1711
1396
|
.option('-v, --verbose', 'verbose output')
|
|
1712
1397
|
.action(async (name, options) => {
|
|
@@ -1714,6 +1399,7 @@ export async function main() {
|
|
|
1714
1399
|
await runInit(name, {
|
|
1715
1400
|
verbose: options['verbose'] ?? false,
|
|
1716
1401
|
yes: options['yes'] ?? false,
|
|
1402
|
+
packageManager: parsePackageManager(options['packageManager']),
|
|
1717
1403
|
...(agent !== undefined ? { agent } : {}),
|
|
1718
1404
|
});
|
|
1719
1405
|
});
|
|
@@ -1830,39 +1516,6 @@ function validateArgs(args) {
|
|
|
1830
1516
|
}
|
|
1831
1517
|
}
|
|
1832
1518
|
}
|
|
1833
|
-
function spawnInherited(cmd, args, cwd, activityLabel = cmd) {
|
|
1834
|
-
const spawnSpec = resolveSpawnSpec(cmd, args);
|
|
1835
|
-
const child = spawn(spawnSpec.command, spawnSpec.args, {
|
|
1836
|
-
stdio: 'inherit',
|
|
1837
|
-
...(spawnSpec.shell !== undefined ? { shell: spawnSpec.shell } : {}),
|
|
1838
|
-
...(cwd ? { cwd } : {}),
|
|
1839
|
-
});
|
|
1840
|
-
const childSignals = forwardChildSignals(child, activityLabel);
|
|
1841
|
-
return new Promise((resolve, reject) => {
|
|
1842
|
-
child.on('close', (code, signal) => {
|
|
1843
|
-
const forwardedSignal = childSignals.getForwardedSignal();
|
|
1844
|
-
childSignals.cleanup();
|
|
1845
|
-
if (forwardedSignal) {
|
|
1846
|
-
process.kill(process.pid, forwardedSignal);
|
|
1847
|
-
return;
|
|
1848
|
-
}
|
|
1849
|
-
if (signal) {
|
|
1850
|
-
process.kill(process.pid, signal);
|
|
1851
|
-
return;
|
|
1852
|
-
}
|
|
1853
|
-
else if (code === 0) {
|
|
1854
|
-
resolve();
|
|
1855
|
-
}
|
|
1856
|
-
else {
|
|
1857
|
-
reject(new Error(`${cmd} exited with code ${code}`));
|
|
1858
|
-
}
|
|
1859
|
-
});
|
|
1860
|
-
child.on('error', (err) => {
|
|
1861
|
-
childSignals.cleanup();
|
|
1862
|
-
reject(err);
|
|
1863
|
-
});
|
|
1864
|
-
});
|
|
1865
|
-
}
|
|
1866
1519
|
async function run(command, additionalArgs, customConfigPath, verbose = false, mockRecord = false) {
|
|
1867
1520
|
const configPath = findScreenCIConfig(customConfigPath);
|
|
1868
1521
|
if (!configPath) {
|
|
@@ -1902,6 +1555,11 @@ async function run(command, additionalArgs, customConfigPath, verbose = false, m
|
|
|
1902
1555
|
stdio: 'inherit',
|
|
1903
1556
|
...(process.platform !== 'win32' ? { detached: true } : {}),
|
|
1904
1557
|
...(spawnSpec.shell !== undefined ? { shell: spawnSpec.shell } : {}),
|
|
1558
|
+
...(spawnSpec.windowsVerbatimArguments !== undefined
|
|
1559
|
+
? {
|
|
1560
|
+
windowsVerbatimArguments: spawnSpec.windowsVerbatimArguments,
|
|
1561
|
+
}
|
|
1562
|
+
: {}),
|
|
1905
1563
|
env: {
|
|
1906
1564
|
...envForChild,
|
|
1907
1565
|
// Enable recording only for record command
|