screenci 0.0.62 → 0.0.64
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 +34 -15
- package/bin/screenci.js +2 -2
- package/dist/cli.d.ts +46 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +463 -242
- package/dist/cli.js.map +1 -1
- package/dist/docs/manifest.d.ts +132 -70
- package/dist/docs/manifest.d.ts.map +1 -1
- package/dist/docs/manifest.js +46 -24
- package/dist/docs/manifest.js.map +1 -1
- package/dist/docs/videos.d.ts +1 -1
- package/dist/docs/videos.js +1 -1
- package/dist/docs/videos.js.map +1 -1
- package/dist/e2e/instrument.e2e.js +11 -11
- package/dist/e2e/instrument.e2e.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/src/asset.d.ts +27 -0
- package/dist/src/asset.d.ts.map +1 -1
- package/dist/src/asset.js +46 -0
- package/dist/src/asset.js.map +1 -1
- package/dist/src/changeFocus.d.ts.map +1 -1
- package/dist/src/changeFocus.js +3 -3
- package/dist/src/changeFocus.js.map +1 -1
- package/dist/src/cue.d.ts +60 -13
- package/dist/src/cue.d.ts.map +1 -1
- package/dist/src/cue.js +153 -47
- package/dist/src/cue.js.map +1 -1
- package/dist/src/events.d.ts +56 -8
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +47 -1
- package/dist/src/events.js.map +1 -1
- package/dist/src/git.d.ts +15 -0
- package/dist/src/git.d.ts.map +1 -0
- package/dist/src/git.js +43 -0
- package/dist/src/git.js.map +1 -0
- package/dist/src/init.d.ts +9 -0
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +293 -113
- package/dist/src/init.js.map +1 -1
- package/dist/src/instrument.d.ts.map +1 -1
- package/dist/src/instrument.js +49 -125
- package/dist/src/instrument.js.map +1 -1
- package/dist/src/mouse.d.ts +1 -0
- package/dist/src/mouse.d.ts.map +1 -1
- package/dist/src/mouse.js +9 -3
- package/dist/src/mouse.js.map +1 -1
- package/dist/src/recording.d.ts +1 -1
- package/dist/src/recording.d.ts.map +1 -1
- package/dist/src/recordingData.d.ts +43 -1
- package/dist/src/recordingData.d.ts.map +1 -1
- package/dist/src/studio.d.ts +36 -0
- package/dist/src/studio.d.ts.map +1 -0
- package/dist/src/studio.js +39 -0
- package/dist/src/studio.js.map +1 -0
- package/dist/src/types.d.ts +141 -125
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/video.d.ts +2 -1
- package/dist/src/video.d.ts.map +1 -1
- package/dist/src/video.js.map +1 -1
- package/dist/src/voices.d.ts +3 -3
- package/dist/src/voices.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/skills/screenci/SKILL.md +7 -8
- package/skills/screenci/references/init.md +1 -2
- package/skills/screenci/references/record.md +3 -9
package/dist/src/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import { existsSync, readFileSync, realpathSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, rmSync } from 'fs';
|
|
3
3
|
import { appendFile, mkdir, readFile, writeFile } from 'fs/promises';
|
|
4
|
-
import { basename, delimiter, dirname, resolve } from 'path';
|
|
4
|
+
import { basename, delimiter, dirname, relative, resolve, sep } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { Command, CommanderError } from 'commander';
|
|
7
7
|
import { input } from '@inquirer/prompts';
|
|
@@ -47,14 +47,22 @@ export function detectPackageManagerFromPackageJson(dir) {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
export function determinePackageManager(cwd) {
|
|
50
|
+
// 1. The package manager that spawned the process is explicit intent
|
|
51
|
+
// (`pnpm create screenci`, `yarn create screenci`, `npm init screenci`) and
|
|
52
|
+
// always wins — including npm, so `npm init` gives an npm island even in a
|
|
53
|
+
// pnpm/yarn repo. Check pnpm/yarn before npm because their user-agent
|
|
54
|
+
// strings also contain an `npm/...` segment.
|
|
50
55
|
const userAgent = process.env.npm_config_user_agent;
|
|
51
56
|
if (userAgent?.includes('pnpm'))
|
|
52
57
|
return 'pnpm';
|
|
53
58
|
if (userAgent?.includes('yarn'))
|
|
54
59
|
return 'yarn';
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
60
|
+
if (userAgent?.includes('npm'))
|
|
61
|
+
return 'npm';
|
|
62
|
+
// 2. No package-manager wrapper set a user agent (e.g. a global or direct
|
|
63
|
+
// `screenci init`). Fall back to the surrounding repo's toolchain so the
|
|
64
|
+
// island matches it, then to npm. Pass `--package-manager` to override.
|
|
65
|
+
// (record/test pass no cwd and rely solely on the user agent.)
|
|
58
66
|
if (cwd !== undefined) {
|
|
59
67
|
const fromLockfile = detectPackageManagerFromLockfile(cwd);
|
|
60
68
|
if (fromLockfile)
|
|
@@ -77,15 +85,31 @@ export function detectPnpmWorkspace(cwd) {
|
|
|
77
85
|
}
|
|
78
86
|
return false;
|
|
79
87
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Locate the repository root by walking up from `startDir` until a `.git`
|
|
90
|
+
* directory is found. Used to place the GitHub Actions workflow (which GitHub
|
|
91
|
+
* only discovers at the repo root) and the agent skills. Falls back to
|
|
92
|
+
* `startDir` when no `.git` is found.
|
|
93
|
+
*/
|
|
94
|
+
function findRepoRoot(startDir) {
|
|
95
|
+
let current = startDir;
|
|
96
|
+
while (true) {
|
|
97
|
+
if (existsSync(resolve(current, '.git')))
|
|
98
|
+
return current;
|
|
99
|
+
const parent = dirname(current);
|
|
100
|
+
if (parent === current)
|
|
101
|
+
break;
|
|
102
|
+
current = parent;
|
|
88
103
|
}
|
|
104
|
+
return startDir;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Convert a filesystem-relative path to a POSIX-style path suitable for YAML
|
|
108
|
+
* `working-directory` / `cache-dependency-path` fields in the workflow.
|
|
109
|
+
*/
|
|
110
|
+
function toWorkflowPath(relativePath) {
|
|
111
|
+
const normalized = relativePath.split(sep).join('/');
|
|
112
|
+
return normalized.length === 0 ? '.' : normalized;
|
|
89
113
|
}
|
|
90
114
|
function resolveSpawnSpec(cmd, args) {
|
|
91
115
|
if (process.platform !== 'win32') {
|
|
@@ -314,7 +338,12 @@ function getPackageManagerCommand(packageManager, isWorkspace = false) {
|
|
|
314
338
|
screenciRun: 'pnpm exec screenci',
|
|
315
339
|
playwrightRun: 'pnpm exec playwright',
|
|
316
340
|
installCommand: 'pnpm',
|
|
317
|
-
installArgs: (
|
|
341
|
+
installArgs: (...pkgs) => [
|
|
342
|
+
'add',
|
|
343
|
+
'--save-dev',
|
|
344
|
+
...workspaceFlag,
|
|
345
|
+
...pkgs,
|
|
346
|
+
],
|
|
318
347
|
screenciInstallArgs: (pkg) => [
|
|
319
348
|
'add',
|
|
320
349
|
'--save-dev',
|
|
@@ -343,7 +372,7 @@ function getPackageManagerCommand(packageManager, isWorkspace = false) {
|
|
|
343
372
|
screenciRun: 'yarn screenci',
|
|
344
373
|
playwrightRun: 'yarn playwright',
|
|
345
374
|
installCommand: 'yarn',
|
|
346
|
-
installArgs: (
|
|
375
|
+
installArgs: (...pkgs) => ['add', '--dev', ...workspaceFlag, ...pkgs],
|
|
347
376
|
screenciInstallArgs: (pkg) => ['add', '--dev', ...workspaceFlag, pkg],
|
|
348
377
|
skillsCommand: 'yarn',
|
|
349
378
|
skillsArgs: (skills, agent) => [
|
|
@@ -364,7 +393,7 @@ function getPackageManagerCommand(packageManager, isWorkspace = false) {
|
|
|
364
393
|
screenciRun: 'npx screenci',
|
|
365
394
|
playwrightRun: 'npx playwright',
|
|
366
395
|
installCommand: 'npm',
|
|
367
|
-
installArgs: (
|
|
396
|
+
installArgs: (...pkgs) => ['install', '--save-dev', ...pkgs],
|
|
368
397
|
screenciInstallArgs: (pkg) => ['install', '--save-dev', pkg],
|
|
369
398
|
skillsCommand: 'npm',
|
|
370
399
|
skillsArgs: (skills, agent) => [
|
|
@@ -405,21 +434,112 @@ function getSkillsManualCommand(packageManager, skills, agent) {
|
|
|
405
434
|
.concat(['-y'])
|
|
406
435
|
.join(' ');
|
|
407
436
|
}
|
|
408
|
-
|
|
409
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Turn a human project name into a valid npm package name for the island's own
|
|
439
|
+
* package.json. We use the project name (the repository root directory name by
|
|
440
|
+
* default) directly. The name must NOT be `screenci` (a package cannot depend
|
|
441
|
+
* on a package with its own name), so we fall back to `screenci-videos` only
|
|
442
|
+
* when the slug is empty or would collide with `screenci`.
|
|
443
|
+
*/
|
|
444
|
+
export function toIslandPackageName(projectName) {
|
|
445
|
+
const slug = projectName
|
|
446
|
+
.toLowerCase()
|
|
447
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
448
|
+
.replace(/^-+|-+$/g, '');
|
|
449
|
+
if (slug.length === 0 || slug === 'screenci') {
|
|
450
|
+
return 'screenci-videos';
|
|
451
|
+
}
|
|
452
|
+
return slug;
|
|
410
453
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
454
|
+
function generateIslandPackageJson(projectName) {
|
|
455
|
+
return (JSON.stringify({
|
|
456
|
+
name: toIslandPackageName(projectName),
|
|
457
|
+
private: true,
|
|
458
|
+
type: 'module',
|
|
459
|
+
scripts: {
|
|
460
|
+
test: 'screenci test',
|
|
461
|
+
record: 'screenci record',
|
|
462
|
+
},
|
|
463
|
+
}, null, 2) + '\n');
|
|
464
|
+
}
|
|
465
|
+
function generateIslandTsconfig() {
|
|
466
|
+
// Minimal config so an editor type-checks the island as its own project
|
|
467
|
+
// instead of inheriting a surrounding repo's tsconfig or the legacy TS
|
|
468
|
+
// defaults. `module`/`moduleResolution` let TypeScript read screenci's ESM
|
|
469
|
+
// `exports` map; `target` gives the example's async/await a modern lib.
|
|
470
|
+
return (JSON.stringify({
|
|
471
|
+
compilerOptions: {
|
|
472
|
+
module: 'ESNext',
|
|
473
|
+
moduleResolution: 'bundler',
|
|
474
|
+
target: 'ESNext',
|
|
475
|
+
},
|
|
476
|
+
}, null, 2) + '\n');
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Resolve the package-manager-specific command a user types to run the island's
|
|
480
|
+
* own `test` / `record` scripts. npm needs `run` for non-`test` scripts and `--`
|
|
481
|
+
* to forward flags; pnpm and yarn forward both implicitly.
|
|
482
|
+
*/
|
|
483
|
+
function getIslandScriptInvocations(packageManager) {
|
|
484
|
+
if (packageManager === 'pnpm') {
|
|
485
|
+
return {
|
|
486
|
+
test: 'pnpm test',
|
|
487
|
+
testUi: 'pnpm test --ui',
|
|
488
|
+
record: 'pnpm record',
|
|
489
|
+
};
|
|
419
490
|
}
|
|
420
|
-
|
|
421
|
-
|
|
491
|
+
if (packageManager === 'yarn') {
|
|
492
|
+
return {
|
|
493
|
+
test: 'yarn test',
|
|
494
|
+
testUi: 'yarn test --ui',
|
|
495
|
+
record: 'yarn record',
|
|
496
|
+
};
|
|
422
497
|
}
|
|
498
|
+
return {
|
|
499
|
+
test: 'npm test',
|
|
500
|
+
testUi: 'npm test -- --ui',
|
|
501
|
+
record: 'npm run record',
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
export function generateIslandReadme(projectName, packageManager) {
|
|
505
|
+
const scripts = getIslandScriptInvocations(packageManager);
|
|
506
|
+
return `# ${projectName}
|
|
507
|
+
|
|
508
|
+
ScreenCI video scripts for this project. Edit the \`*.video.ts\` files in
|
|
509
|
+
\`videos/\` to script your recordings.
|
|
510
|
+
|
|
511
|
+
## Commands
|
|
512
|
+
|
|
513
|
+
- \`${scripts.test}\` tests your video scripts fast locally.
|
|
514
|
+
- \`${scripts.testUi}\` tests your video scripts in interactive UI mode.
|
|
515
|
+
- \`${scripts.record}\` records and pauses for first-time setup if needed.
|
|
516
|
+
|
|
517
|
+
## Learn more
|
|
518
|
+
|
|
519
|
+
Visit https://screenci.com/docs for the full documentation.
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
function generatePnpmWorkspaceYaml(pnpmMajor) {
|
|
523
|
+
// A nested `pnpm-workspace.yaml` makes pnpm treat the island as its own
|
|
524
|
+
// workspace root, so a surrounding monorepo workspace does not absorb it (no
|
|
525
|
+
// hoisting, no `-w` install). It also pre-approves the ffmpeg-static build
|
|
526
|
+
// script so non-interactive installs (e.g. `pnpm install --frozen-lockfile`
|
|
527
|
+
// in CI) build the bundled binary without prompting.
|
|
528
|
+
//
|
|
529
|
+
// pnpm 10 and 11 spell this approval differently: pnpm 11 removed
|
|
530
|
+
// `onlyBuiltDependencies` in favour of the `allowBuilds` map. Emit the key
|
|
531
|
+
// that matches the installed pnpm so the approval is actually honoured.
|
|
532
|
+
const buildApproval = pnpmMajor >= 11
|
|
533
|
+
? `allowBuilds:
|
|
534
|
+
ffmpeg-static: true
|
|
535
|
+
`
|
|
536
|
+
: `onlyBuiltDependencies:
|
|
537
|
+
- ffmpeg-static
|
|
538
|
+
`;
|
|
539
|
+
return `packages:
|
|
540
|
+
- '.'
|
|
541
|
+
|
|
542
|
+
${buildApproval}`;
|
|
423
543
|
}
|
|
424
544
|
function parseSemverTriplet(version) {
|
|
425
545
|
const match = version
|
|
@@ -522,6 +642,15 @@ async function ensureSupportedPnpmVersion(cwd) {
|
|
|
522
642
|
if (!versionSupport.supported) {
|
|
523
643
|
throw buildUnsupportedPnpmError(versionSupport);
|
|
524
644
|
}
|
|
645
|
+
return versionSupport;
|
|
646
|
+
}
|
|
647
|
+
// A supported pnpm version always parses (the support check rejects malformed
|
|
648
|
+
// versions), so the major is reliable here; fall back to 10 defensively.
|
|
649
|
+
function pnpmMajorFromSupport(versionSupport) {
|
|
650
|
+
const parsed = versionSupport.detectedVersion
|
|
651
|
+
? parseSemverTriplet(versionSupport.detectedVersion)
|
|
652
|
+
: null;
|
|
653
|
+
return parsed?.[0] ?? 10;
|
|
525
654
|
}
|
|
526
655
|
export function parseYarnVersionSupport(versionOutput) {
|
|
527
656
|
const detectedVersion = versionOutput.trim();
|
|
@@ -642,26 +771,27 @@ async function writeInitGitignore(projectDir, packageManager) {
|
|
|
642
771
|
await appendFile(gitignorePath, `${separator}${content}`);
|
|
643
772
|
}
|
|
644
773
|
async function installInitDependencies(projectDir, verbose, screenciDependency, includePlaywrightCli, commands) {
|
|
774
|
+
// Packages that share identical install flags are installed in a single
|
|
775
|
+
// command so the package manager resolves the dependency graph once instead
|
|
776
|
+
// of once per package. ScreenCI stays separate because on pnpm it needs an
|
|
777
|
+
// extra '--allow-build=ffmpeg-static' flag the others don't carry.
|
|
778
|
+
const sharedPackages = [
|
|
779
|
+
`@playwright/test@${PLAYWRIGHT_TEST_VERSION}`,
|
|
780
|
+
`@types/node@${NODE_TYPES_VERSION}`,
|
|
781
|
+
...(includePlaywrightCli
|
|
782
|
+
? [`@playwright/cli@${PLAYWRIGHT_CLI_VERSION}`]
|
|
783
|
+
: []),
|
|
784
|
+
];
|
|
645
785
|
const installSteps = [
|
|
646
786
|
{
|
|
647
|
-
message: 'Installing
|
|
648
|
-
args: commands.installArgs(
|
|
787
|
+
message: 'Installing dependencies...',
|
|
788
|
+
args: commands.installArgs(...sharedPackages),
|
|
649
789
|
},
|
|
650
790
|
{
|
|
651
791
|
message: 'Installing ScreenCI...',
|
|
652
792
|
args: commands.screenciInstallArgs(`screenci@${screenciDependency}`),
|
|
653
793
|
},
|
|
654
|
-
{
|
|
655
|
-
message: 'Installing Node.js types...',
|
|
656
|
-
args: commands.installArgs(`@types/node@${NODE_TYPES_VERSION}`),
|
|
657
|
-
},
|
|
658
794
|
];
|
|
659
|
-
if (includePlaywrightCli) {
|
|
660
|
-
installSteps.push({
|
|
661
|
-
message: 'Installing playwright-cli...',
|
|
662
|
-
args: commands.installArgs(`@playwright/cli@${PLAYWRIGHT_CLI_VERSION}`),
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
795
|
for (const step of installSteps) {
|
|
666
796
|
if (verbose) {
|
|
667
797
|
logger.info(`Running '${commands.installCommand} ${step.args.join(' ')}'...`);
|
|
@@ -680,12 +810,12 @@ async function installInitDependencies(projectDir, verbose, screenciDependency,
|
|
|
680
810
|
}
|
|
681
811
|
}
|
|
682
812
|
}
|
|
683
|
-
function printInitNextSteps(projectDir, packageManager) {
|
|
813
|
+
function printInitNextSteps(projectDir, islandDirName, packageManager) {
|
|
684
814
|
const resolvedProjectDir = realpathSync(projectDir);
|
|
685
815
|
const commands = getPackageManagerCommand(packageManager);
|
|
686
816
|
logger.info(`${pc.green('✔ Success!')} Created a ScreenCI project at ${resolvedProjectDir}`);
|
|
687
817
|
logger.info('');
|
|
688
|
-
logger.info('
|
|
818
|
+
logger.info('You can now run these commands:');
|
|
689
819
|
logger.info('');
|
|
690
820
|
logger.info(` ${pc.cyan(`${commands.screenciRun} test`)}`);
|
|
691
821
|
logger.info(' Tests your video scripts fast locally.');
|
|
@@ -693,26 +823,28 @@ function printInitNextSteps(projectDir, packageManager) {
|
|
|
693
823
|
logger.info(` ${pc.cyan(`${commands.screenciRun} test --ui`)}`);
|
|
694
824
|
logger.info(' Tests your video scripts in interactive UI mode.');
|
|
695
825
|
logger.info('');
|
|
696
|
-
logger.info(` ${pc.cyan(`${commands.screenciRun} login`)}`);
|
|
697
|
-
logger.info(' Saves SCREENCI_SECRET for uploads and remote rendering.');
|
|
698
|
-
logger.info('');
|
|
699
826
|
logger.info(` ${pc.cyan(`${commands.screenciRun} record`)}`);
|
|
700
|
-
logger.info(' Records
|
|
827
|
+
logger.info(' Records locally and pauses for first-time ScreenCI setup if needed.');
|
|
701
828
|
logger.info('');
|
|
702
829
|
logger.info('We suggest that you begin by typing:');
|
|
703
830
|
logger.info('');
|
|
831
|
+
logger.info(` ${pc.cyan(`cd ${islandDirName}`)}`);
|
|
704
832
|
logger.info(` ${pc.cyan(`${commands.screenciRun} test`)}`);
|
|
705
833
|
logger.info('');
|
|
706
834
|
logger.info('And check out the following files:');
|
|
707
|
-
logger.info(
|
|
708
|
-
logger.info(
|
|
835
|
+
logger.info(` - ./${islandDirName}/videos/example.video.ts - Example video script`);
|
|
836
|
+
logger.info(` - ./${islandDirName}/screenci.config.ts - ScreenCI configuration`);
|
|
837
|
+
logger.info(` - ./${islandDirName}/README.md - Project commands and docs link`);
|
|
709
838
|
logger.info('');
|
|
710
839
|
logger.info(`Visit ${pc.cyan('https://screenci.com/docs')} for more information.`);
|
|
711
840
|
logger.info('');
|
|
712
841
|
logger.info('Happy hacking! 🎥');
|
|
713
842
|
}
|
|
714
|
-
function generateGithubAction(packageManager,
|
|
715
|
-
const commands = getPackageManagerCommand(packageManager
|
|
843
|
+
function generateGithubAction(packageManager, islandWorkflowPath) {
|
|
844
|
+
const commands = getPackageManagerCommand(packageManager);
|
|
845
|
+
const cacheDependencyPath = islandWorkflowPath === '.'
|
|
846
|
+
? commands.lockfileName
|
|
847
|
+
: `${islandWorkflowPath}/${commands.lockfileName}`;
|
|
716
848
|
return `name: ScreenCI
|
|
717
849
|
|
|
718
850
|
on:
|
|
@@ -742,22 +874,22 @@ jobs:
|
|
|
742
874
|
with:
|
|
743
875
|
node-version: 24
|
|
744
876
|
cache: ${commands.cacheName}
|
|
745
|
-
cache-dependency-path: ${
|
|
877
|
+
cache-dependency-path: ${cacheDependencyPath}
|
|
746
878
|
|
|
747
879
|
- name: Install dependencies
|
|
748
|
-
working-directory:
|
|
880
|
+
working-directory: ${islandWorkflowPath}
|
|
749
881
|
env:
|
|
750
882
|
HUSKY: 0
|
|
751
883
|
npm_config_strict_dep_builds: false
|
|
752
884
|
run: ${commands.frozenInstallCommand}
|
|
753
885
|
|
|
754
886
|
- name: Install Chromium Headless Shell
|
|
755
|
-
working-directory:
|
|
887
|
+
working-directory: ${islandWorkflowPath}
|
|
756
888
|
run: ${commands.playwrightRun} install --only-shell chromium
|
|
757
889
|
|
|
758
890
|
- id: record
|
|
759
891
|
name: Record
|
|
760
|
-
working-directory:
|
|
892
|
+
working-directory: ${islandWorkflowPath}
|
|
761
893
|
env:
|
|
762
894
|
SCREENCI_SECRET: \${{ secrets.SCREENCI_SECRET }}
|
|
763
895
|
run: ${commands.screenciRun} record
|
|
@@ -857,12 +989,20 @@ function getInitScreenciDependencyOverride() {
|
|
|
857
989
|
export async function runInit(projectNameArg, options) {
|
|
858
990
|
const { verbose, yes, agent, packageManager } = options;
|
|
859
991
|
const initCwd = getInitProjectRoot();
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
992
|
+
const commands = getPackageManagerCommand(packageManager);
|
|
993
|
+
// ScreenCI scaffolds a self-contained `screenci/` island under the current
|
|
994
|
+
// directory: its own package.json + local install, deliberately NOT a member
|
|
995
|
+
// of any surrounding workspace. This keeps installation deterministic in
|
|
996
|
+
// monorepos. The GitHub workflow and agent skills, however, must live at the
|
|
997
|
+
// repository root (that is where GitHub and coding agents discover them).
|
|
998
|
+
const repoRoot = findRepoRoot(initCwd);
|
|
999
|
+
const islandDir = resolve(initCwd, 'screenci');
|
|
1000
|
+
const islandDirName = toWorkflowPath(relative(initCwd, islandDir));
|
|
1001
|
+
const islandWorkflowPath = toWorkflowPath(relative(repoRoot, islandDir));
|
|
1002
|
+
if (existsSync(islandDir)) {
|
|
1003
|
+
logger.error(`Error: ${islandDirName}/ already exists. Remove it (or run init in a different directory) and try again.`);
|
|
1004
|
+
process.exit(1);
|
|
1005
|
+
}
|
|
866
1006
|
if (packageManager === 'yarn') {
|
|
867
1007
|
await ensureSupportedYarnVersion(initCwd);
|
|
868
1008
|
}
|
|
@@ -874,8 +1014,7 @@ export async function runInit(projectNameArg, options) {
|
|
|
874
1014
|
logger.error('Error: Project name is required');
|
|
875
1015
|
process.exit(1);
|
|
876
1016
|
}
|
|
877
|
-
const
|
|
878
|
-
const githubWorkflowsDir = resolve(projectDir, '.github', 'workflows');
|
|
1017
|
+
const githubWorkflowsDir = resolve(repoRoot, '.github', 'workflows');
|
|
879
1018
|
const githubActionPath = resolve(githubWorkflowsDir, 'screenci.yaml');
|
|
880
1019
|
const shouldAddGithubActionWorkflow = yes
|
|
881
1020
|
? true
|
|
@@ -892,10 +1031,13 @@ export async function runInit(projectNameArg, options) {
|
|
|
892
1031
|
const shouldInstallPlaywrightCli = yes
|
|
893
1032
|
? true
|
|
894
1033
|
: await promptInitPlaywrightCliSkillForPackageManager(packageManager, agent);
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1034
|
+
// The workflow lives at the repo root. If one already exists, skip it (do not
|
|
1035
|
+
// overwrite, do not fail) so re-running init stays non-destructive.
|
|
1036
|
+
const workflowAlreadyExists = shouldAddGithubActionWorkflow && existsSync(githubActionPath);
|
|
1037
|
+
if (workflowAlreadyExists) {
|
|
1038
|
+
logger.info(`Skipping GitHub Actions workflow: ${toWorkflowPath(relative(repoRoot, githubActionPath))} already exists`);
|
|
898
1039
|
}
|
|
1040
|
+
const shouldWriteGithubActionWorkflow = shouldAddGithubActionWorkflow && !workflowAlreadyExists;
|
|
899
1041
|
const skills = [];
|
|
900
1042
|
if (shouldInstallScreenCISkill) {
|
|
901
1043
|
skills.push('screenci');
|
|
@@ -908,61 +1050,99 @@ export async function runInit(projectNameArg, options) {
|
|
|
908
1050
|
? null
|
|
909
1051
|
: `${commands.skillsCommand} ${skillsArgs.join(' ')}`;
|
|
910
1052
|
const screenciDependency = getInitScreenciDependencyOverride() ?? (await readCurrentScreenciVersion());
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
await ensurePackageJsonTypeModule(packageJsonPath);
|
|
923
|
-
}
|
|
924
|
-
await writeInitGitignore(projectDir, packageManager);
|
|
925
|
-
await writeFile(resolve(projectDir, 'videos', 'example.video.ts'), generateExampleVideo());
|
|
926
|
-
if (shouldAddGithubActionWorkflow) {
|
|
927
|
-
await writeFile(githubActionPath, generateGithubAction(packageManager, isWorkspace));
|
|
928
|
-
}
|
|
929
|
-
if (packageManager === 'pnpm') {
|
|
930
|
-
await ensureSupportedPnpmVersion(projectDir);
|
|
931
|
-
}
|
|
932
|
-
if (packageManager === 'yarn') {
|
|
933
|
-
await writeFile(resolve(projectDir, '.yarnrc.yml'), 'nodeLinker: node-modules\n');
|
|
934
|
-
}
|
|
935
|
-
if (skillsArgs !== null) {
|
|
936
|
-
if (verbose) {
|
|
937
|
-
logger.info(`Running '${skillsCommand}'...`);
|
|
938
|
-
await spawnInherited(commands.skillsCommand, skillsArgs, projectDir, 'screenci init');
|
|
1053
|
+
// Everything below creates files / runs installs. If anything fails (or the
|
|
1054
|
+
// user interrupts), roll back the `screenci/` directory we created so the
|
|
1055
|
+
// next `init` run starts from a clean slate. Pre-existing repo-root files
|
|
1056
|
+
// (e.g. .github/, .claude/) are left untouched.
|
|
1057
|
+
let islandCreated = false;
|
|
1058
|
+
let scaffoldComplete = false;
|
|
1059
|
+
const removePartialIsland = () => {
|
|
1060
|
+
if (!islandCreated || scaffoldComplete)
|
|
1061
|
+
return;
|
|
1062
|
+
try {
|
|
1063
|
+
rmSync(islandDir, { recursive: true, force: true });
|
|
939
1064
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1065
|
+
catch {
|
|
1066
|
+
// best-effort cleanup
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
const onSigint = () => {
|
|
1070
|
+
removePartialIsland();
|
|
1071
|
+
process.exit(130);
|
|
1072
|
+
};
|
|
1073
|
+
const onSigterm = () => {
|
|
1074
|
+
removePartialIsland();
|
|
1075
|
+
process.exit(143);
|
|
1076
|
+
};
|
|
1077
|
+
process.on('SIGINT', onSigint);
|
|
1078
|
+
process.on('SIGTERM', onSigterm);
|
|
1079
|
+
process.on('exit', removePartialIsland);
|
|
1080
|
+
try {
|
|
1081
|
+
await mkdir(resolve(islandDir, 'videos'), { recursive: true });
|
|
1082
|
+
islandCreated = true;
|
|
1083
|
+
await writeFile(resolve(islandDir, 'screenci.config.ts'), generateConfig(projectName));
|
|
1084
|
+
await writeFile(resolve(islandDir, 'package.json'), generateIslandPackageJson(projectName));
|
|
1085
|
+
await writeFile(resolve(islandDir, 'tsconfig.json'), generateIslandTsconfig());
|
|
1086
|
+
await writeFile(resolve(islandDir, 'README.md'), generateIslandReadme(projectName, packageManager));
|
|
1087
|
+
await writeInitGitignore(islandDir, packageManager);
|
|
1088
|
+
await writeFile(resolve(islandDir, 'videos', 'example.video.ts'), generateExampleVideo());
|
|
1089
|
+
if (packageManager === 'pnpm') {
|
|
1090
|
+
// Resolve (and gate on) the pnpm version before writing the workspace
|
|
1091
|
+
// file so the build-approval key matches the installed pnpm.
|
|
1092
|
+
const pnpmVersionSupport = await ensureSupportedPnpmVersion(islandDir);
|
|
1093
|
+
await writeFile(resolve(islandDir, 'pnpm-workspace.yaml'), generatePnpmWorkspaceYaml(pnpmMajorFromSupport(pnpmVersionSupport)));
|
|
1094
|
+
}
|
|
1095
|
+
if (packageManager === 'yarn') {
|
|
1096
|
+
await writeFile(resolve(islandDir, '.yarnrc.yml'), 'nodeLinker: node-modules\n');
|
|
1097
|
+
}
|
|
1098
|
+
if (shouldWriteGithubActionWorkflow) {
|
|
1099
|
+
await mkdir(githubWorkflowsDir, { recursive: true });
|
|
1100
|
+
await writeFile(githubActionPath, generateGithubAction(packageManager, islandWorkflowPath));
|
|
1101
|
+
}
|
|
1102
|
+
// Install skills at the repo root so coding agents discover them when the
|
|
1103
|
+
// repository is opened as the workspace.
|
|
1104
|
+
if (skillsArgs !== null) {
|
|
1105
|
+
if (verbose) {
|
|
1106
|
+
logger.info(`Running '${skillsCommand}'...`);
|
|
1107
|
+
await spawnInherited(commands.skillsCommand, skillsArgs, repoRoot, 'screenci init');
|
|
945
1108
|
}
|
|
946
|
-
|
|
947
|
-
spinner
|
|
948
|
-
|
|
1109
|
+
else {
|
|
1110
|
+
const spinner = ora('Adding selected AI skills...').start();
|
|
1111
|
+
try {
|
|
1112
|
+
await spawnSilent(commands.skillsCommand, skillsArgs, repoRoot);
|
|
1113
|
+
spinner.succeed('Installing selected AI skills');
|
|
1114
|
+
}
|
|
1115
|
+
catch (err) {
|
|
1116
|
+
spinner.fail('AI skills install failed');
|
|
1117
|
+
throw err;
|
|
1118
|
+
}
|
|
949
1119
|
}
|
|
950
1120
|
}
|
|
1121
|
+
await installInitDependencies(islandDir, verbose, screenciDependency, shouldInstallPlaywrightCli, commands);
|
|
1122
|
+
if (shouldInstallPlaywrightBrowsers) {
|
|
1123
|
+
logger.info(`Installing Playwright Chromium headless shell with '${commands.playwrightRun} install --only-shell chromium'...`);
|
|
1124
|
+
const [browserCmd, ...browserArgs] = buildPlaywrightSpawnArgs(packageManager, 'install', '--only-shell', 'chromium');
|
|
1125
|
+
await spawnInherited(browserCmd, browserArgs, islandDir, 'screenci init');
|
|
1126
|
+
logger.info(`${pc.green('✔')} Playwright Chromium headless shell installed successfully`);
|
|
1127
|
+
}
|
|
1128
|
+
if (shouldInstallPlaywrightOsDependencies) {
|
|
1129
|
+
logger.info(`Installing Playwright operating system dependencies with '${commands.playwrightRun} install-deps chromium'...`);
|
|
1130
|
+
const [depsCmd, ...depsArgs] = buildPlaywrightSpawnArgs(packageManager, 'install-deps', 'chromium');
|
|
1131
|
+
await spawnInherited(depsCmd, depsArgs, islandDir, 'screenci init');
|
|
1132
|
+
logger.info(`${pc.green('✔')} Playwright operating system dependencies installed successfully`);
|
|
1133
|
+
}
|
|
1134
|
+
scaffoldComplete = true;
|
|
951
1135
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const [browserCmd, ...browserArgs] = buildPlaywrightSpawnArgs(packageManager, 'install', '--only-shell', 'chromium');
|
|
956
|
-
await spawnInherited(browserCmd, browserArgs, projectDir, 'screenci init');
|
|
957
|
-
logger.info(`${pc.green('✔')} Playwright Chromium headless shell installed successfully`);
|
|
1136
|
+
catch (err) {
|
|
1137
|
+
removePartialIsland();
|
|
1138
|
+
throw err;
|
|
958
1139
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
logger.info(`${pc.green('✔')} Playwright operating system dependencies installed successfully`);
|
|
1140
|
+
finally {
|
|
1141
|
+
process.off('SIGINT', onSigint);
|
|
1142
|
+
process.off('SIGTERM', onSigterm);
|
|
1143
|
+
process.off('exit', removePartialIsland);
|
|
964
1144
|
}
|
|
965
|
-
printInitNextSteps(
|
|
1145
|
+
printInitNextSteps(islandDir, islandDirName, packageManager);
|
|
966
1146
|
}
|
|
967
1147
|
function handleCreateCommanderError(err) {
|
|
968
1148
|
if (!(err instanceof CommanderError)) {
|