screenci 0.0.63 → 0.0.65
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 +25 -17
- package/dist/cli.d.ts +17 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +179 -55
- package/dist/cli.js.map +1 -1
- package/dist/docs/manifest.d.ts +92 -61
- package/dist/docs/manifest.d.ts.map +1 -1
- package/dist/docs/manifest.js +32 -21
- 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/src/init.d.ts +9 -0
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +297 -109
- package/dist/src/init.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/skills/screenci/SKILL.md +5 -3
- package/skills/screenci/references/record.md +2 -1
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,117 @@ 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
|
+
// `types: ['node']` makes the `process` global resolve in screenci.config.ts
|
|
471
|
+
// (which reads `process.env.CI`) without depending on whether the editor's TS
|
|
472
|
+
// server auto-discovers @types/node, which is unreliable under pnpm's isolated
|
|
473
|
+
// node_modules layout.
|
|
474
|
+
return (JSON.stringify({
|
|
475
|
+
compilerOptions: {
|
|
476
|
+
module: 'ESNext',
|
|
477
|
+
moduleResolution: 'bundler',
|
|
478
|
+
target: 'ESNext',
|
|
479
|
+
types: ['node'],
|
|
480
|
+
},
|
|
481
|
+
}, null, 2) + '\n');
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Resolve the package-manager-specific command a user types to run the island's
|
|
485
|
+
* own `test` / `record` scripts. npm needs `run` for non-`test` scripts and `--`
|
|
486
|
+
* to forward flags; pnpm and yarn forward both implicitly.
|
|
487
|
+
*/
|
|
488
|
+
function getIslandScriptInvocations(packageManager) {
|
|
489
|
+
if (packageManager === 'pnpm') {
|
|
490
|
+
return {
|
|
491
|
+
test: 'pnpm test',
|
|
492
|
+
testUi: 'pnpm test --ui',
|
|
493
|
+
record: 'pnpm record',
|
|
494
|
+
};
|
|
419
495
|
}
|
|
420
|
-
|
|
421
|
-
|
|
496
|
+
if (packageManager === 'yarn') {
|
|
497
|
+
return {
|
|
498
|
+
test: 'yarn test',
|
|
499
|
+
testUi: 'yarn test --ui',
|
|
500
|
+
record: 'yarn record',
|
|
501
|
+
};
|
|
422
502
|
}
|
|
503
|
+
return {
|
|
504
|
+
test: 'npm test',
|
|
505
|
+
testUi: 'npm test -- --ui',
|
|
506
|
+
record: 'npm run record',
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
export function generateIslandReadme(projectName, packageManager) {
|
|
510
|
+
const scripts = getIslandScriptInvocations(packageManager);
|
|
511
|
+
return `# ${projectName}
|
|
512
|
+
|
|
513
|
+
ScreenCI video scripts for this project. Edit the \`*.video.ts\` files in
|
|
514
|
+
\`videos/\` to script your recordings.
|
|
515
|
+
|
|
516
|
+
## Commands
|
|
517
|
+
|
|
518
|
+
- \`${scripts.test}\` tests your video scripts fast locally.
|
|
519
|
+
- \`${scripts.testUi}\` tests your video scripts in interactive UI mode.
|
|
520
|
+
- \`${scripts.record}\` records and pauses for first-time setup if needed.
|
|
521
|
+
|
|
522
|
+
## Learn more
|
|
523
|
+
|
|
524
|
+
Visit https://screenci.com/docs for the full documentation.
|
|
525
|
+
`;
|
|
526
|
+
}
|
|
527
|
+
function generatePnpmWorkspaceYaml(pnpmMajor) {
|
|
528
|
+
// A nested `pnpm-workspace.yaml` makes pnpm treat the island as its own
|
|
529
|
+
// workspace root, so a surrounding monorepo workspace does not absorb it (no
|
|
530
|
+
// hoisting, no `-w` install). It also pre-approves the ffmpeg-static build
|
|
531
|
+
// script so non-interactive installs (e.g. `pnpm install --frozen-lockfile`
|
|
532
|
+
// in CI) build the bundled binary without prompting.
|
|
533
|
+
//
|
|
534
|
+
// pnpm 10 and 11 spell this approval differently: pnpm 11 removed
|
|
535
|
+
// `onlyBuiltDependencies` in favour of the `allowBuilds` map. Emit the key
|
|
536
|
+
// that matches the installed pnpm so the approval is actually honoured.
|
|
537
|
+
const buildApproval = pnpmMajor >= 11
|
|
538
|
+
? `allowBuilds:
|
|
539
|
+
ffmpeg-static: true
|
|
540
|
+
`
|
|
541
|
+
: `onlyBuiltDependencies:
|
|
542
|
+
- ffmpeg-static
|
|
543
|
+
`;
|
|
544
|
+
return `packages:
|
|
545
|
+
- '.'
|
|
546
|
+
|
|
547
|
+
${buildApproval}`;
|
|
423
548
|
}
|
|
424
549
|
function parseSemverTriplet(version) {
|
|
425
550
|
const match = version
|
|
@@ -522,6 +647,15 @@ async function ensureSupportedPnpmVersion(cwd) {
|
|
|
522
647
|
if (!versionSupport.supported) {
|
|
523
648
|
throw buildUnsupportedPnpmError(versionSupport);
|
|
524
649
|
}
|
|
650
|
+
return versionSupport;
|
|
651
|
+
}
|
|
652
|
+
// A supported pnpm version always parses (the support check rejects malformed
|
|
653
|
+
// versions), so the major is reliable here; fall back to 10 defensively.
|
|
654
|
+
function pnpmMajorFromSupport(versionSupport) {
|
|
655
|
+
const parsed = versionSupport.detectedVersion
|
|
656
|
+
? parseSemverTriplet(versionSupport.detectedVersion)
|
|
657
|
+
: null;
|
|
658
|
+
return parsed?.[0] ?? 10;
|
|
525
659
|
}
|
|
526
660
|
export function parseYarnVersionSupport(versionOutput) {
|
|
527
661
|
const detectedVersion = versionOutput.trim();
|
|
@@ -642,26 +776,27 @@ async function writeInitGitignore(projectDir, packageManager) {
|
|
|
642
776
|
await appendFile(gitignorePath, `${separator}${content}`);
|
|
643
777
|
}
|
|
644
778
|
async function installInitDependencies(projectDir, verbose, screenciDependency, includePlaywrightCli, commands) {
|
|
779
|
+
// Packages that share identical install flags are installed in a single
|
|
780
|
+
// command so the package manager resolves the dependency graph once instead
|
|
781
|
+
// of once per package. ScreenCI stays separate because on pnpm it needs an
|
|
782
|
+
// extra '--allow-build=ffmpeg-static' flag the others don't carry.
|
|
783
|
+
const sharedPackages = [
|
|
784
|
+
`@playwright/test@${PLAYWRIGHT_TEST_VERSION}`,
|
|
785
|
+
`@types/node@${NODE_TYPES_VERSION}`,
|
|
786
|
+
...(includePlaywrightCli
|
|
787
|
+
? [`@playwright/cli@${PLAYWRIGHT_CLI_VERSION}`]
|
|
788
|
+
: []),
|
|
789
|
+
];
|
|
645
790
|
const installSteps = [
|
|
646
791
|
{
|
|
647
|
-
message: 'Installing
|
|
648
|
-
args: commands.installArgs(
|
|
792
|
+
message: 'Installing dependencies...',
|
|
793
|
+
args: commands.installArgs(...sharedPackages),
|
|
649
794
|
},
|
|
650
795
|
{
|
|
651
796
|
message: 'Installing ScreenCI...',
|
|
652
797
|
args: commands.screenciInstallArgs(`screenci@${screenciDependency}`),
|
|
653
798
|
},
|
|
654
|
-
{
|
|
655
|
-
message: 'Installing Node.js types...',
|
|
656
|
-
args: commands.installArgs(`@types/node@${NODE_TYPES_VERSION}`),
|
|
657
|
-
},
|
|
658
799
|
];
|
|
659
|
-
if (includePlaywrightCli) {
|
|
660
|
-
installSteps.push({
|
|
661
|
-
message: 'Installing playwright-cli...',
|
|
662
|
-
args: commands.installArgs(`@playwright/cli@${PLAYWRIGHT_CLI_VERSION}`),
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
800
|
for (const step of installSteps) {
|
|
666
801
|
if (verbose) {
|
|
667
802
|
logger.info(`Running '${commands.installCommand} ${step.args.join(' ')}'...`);
|
|
@@ -680,12 +815,12 @@ async function installInitDependencies(projectDir, verbose, screenciDependency,
|
|
|
680
815
|
}
|
|
681
816
|
}
|
|
682
817
|
}
|
|
683
|
-
function printInitNextSteps(projectDir, packageManager) {
|
|
818
|
+
function printInitNextSteps(projectDir, islandDirName, packageManager) {
|
|
684
819
|
const resolvedProjectDir = realpathSync(projectDir);
|
|
685
820
|
const commands = getPackageManagerCommand(packageManager);
|
|
686
821
|
logger.info(`${pc.green('✔ Success!')} Created a ScreenCI project at ${resolvedProjectDir}`);
|
|
687
822
|
logger.info('');
|
|
688
|
-
logger.info('
|
|
823
|
+
logger.info('You can now run these commands:');
|
|
689
824
|
logger.info('');
|
|
690
825
|
logger.info(` ${pc.cyan(`${commands.screenciRun} test`)}`);
|
|
691
826
|
logger.info(' Tests your video scripts fast locally.');
|
|
@@ -698,18 +833,23 @@ function printInitNextSteps(projectDir, packageManager) {
|
|
|
698
833
|
logger.info('');
|
|
699
834
|
logger.info('We suggest that you begin by typing:');
|
|
700
835
|
logger.info('');
|
|
836
|
+
logger.info(` ${pc.cyan(`cd ${islandDirName}`)}`);
|
|
701
837
|
logger.info(` ${pc.cyan(`${commands.screenciRun} test`)}`);
|
|
702
838
|
logger.info('');
|
|
703
839
|
logger.info('And check out the following files:');
|
|
704
|
-
logger.info(
|
|
705
|
-
logger.info(
|
|
840
|
+
logger.info(` - ./${islandDirName}/videos/example.video.ts - Example video script`);
|
|
841
|
+
logger.info(` - ./${islandDirName}/screenci.config.ts - ScreenCI configuration`);
|
|
842
|
+
logger.info(` - ./${islandDirName}/README.md - Project commands and docs link`);
|
|
706
843
|
logger.info('');
|
|
707
844
|
logger.info(`Visit ${pc.cyan('https://screenci.com/docs')} for more information.`);
|
|
708
845
|
logger.info('');
|
|
709
846
|
logger.info('Happy hacking! 🎥');
|
|
710
847
|
}
|
|
711
|
-
function generateGithubAction(packageManager,
|
|
712
|
-
const commands = getPackageManagerCommand(packageManager
|
|
848
|
+
function generateGithubAction(packageManager, islandWorkflowPath) {
|
|
849
|
+
const commands = getPackageManagerCommand(packageManager);
|
|
850
|
+
const cacheDependencyPath = islandWorkflowPath === '.'
|
|
851
|
+
? commands.lockfileName
|
|
852
|
+
: `${islandWorkflowPath}/${commands.lockfileName}`;
|
|
713
853
|
return `name: ScreenCI
|
|
714
854
|
|
|
715
855
|
on:
|
|
@@ -739,22 +879,22 @@ jobs:
|
|
|
739
879
|
with:
|
|
740
880
|
node-version: 24
|
|
741
881
|
cache: ${commands.cacheName}
|
|
742
|
-
cache-dependency-path: ${
|
|
882
|
+
cache-dependency-path: ${cacheDependencyPath}
|
|
743
883
|
|
|
744
884
|
- name: Install dependencies
|
|
745
|
-
working-directory:
|
|
885
|
+
working-directory: ${islandWorkflowPath}
|
|
746
886
|
env:
|
|
747
887
|
HUSKY: 0
|
|
748
888
|
npm_config_strict_dep_builds: false
|
|
749
889
|
run: ${commands.frozenInstallCommand}
|
|
750
890
|
|
|
751
891
|
- name: Install Chromium Headless Shell
|
|
752
|
-
working-directory:
|
|
892
|
+
working-directory: ${islandWorkflowPath}
|
|
753
893
|
run: ${commands.playwrightRun} install --only-shell chromium
|
|
754
894
|
|
|
755
895
|
- id: record
|
|
756
896
|
name: Record
|
|
757
|
-
working-directory:
|
|
897
|
+
working-directory: ${islandWorkflowPath}
|
|
758
898
|
env:
|
|
759
899
|
SCREENCI_SECRET: \${{ secrets.SCREENCI_SECRET }}
|
|
760
900
|
run: ${commands.screenciRun} record
|
|
@@ -854,12 +994,20 @@ function getInitScreenciDependencyOverride() {
|
|
|
854
994
|
export async function runInit(projectNameArg, options) {
|
|
855
995
|
const { verbose, yes, agent, packageManager } = options;
|
|
856
996
|
const initCwd = getInitProjectRoot();
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
997
|
+
const commands = getPackageManagerCommand(packageManager);
|
|
998
|
+
// ScreenCI scaffolds a self-contained `screenci/` island under the current
|
|
999
|
+
// directory: its own package.json + local install, deliberately NOT a member
|
|
1000
|
+
// of any surrounding workspace. This keeps installation deterministic in
|
|
1001
|
+
// monorepos. The GitHub workflow and agent skills, however, must live at the
|
|
1002
|
+
// repository root (that is where GitHub and coding agents discover them).
|
|
1003
|
+
const repoRoot = findRepoRoot(initCwd);
|
|
1004
|
+
const islandDir = resolve(initCwd, 'screenci');
|
|
1005
|
+
const islandDirName = toWorkflowPath(relative(initCwd, islandDir));
|
|
1006
|
+
const islandWorkflowPath = toWorkflowPath(relative(repoRoot, islandDir));
|
|
1007
|
+
if (existsSync(islandDir)) {
|
|
1008
|
+
logger.error(`Error: ${islandDirName}/ already exists. Remove it (or run init in a different directory) and try again.`);
|
|
1009
|
+
process.exit(1);
|
|
1010
|
+
}
|
|
863
1011
|
if (packageManager === 'yarn') {
|
|
864
1012
|
await ensureSupportedYarnVersion(initCwd);
|
|
865
1013
|
}
|
|
@@ -871,8 +1019,7 @@ export async function runInit(projectNameArg, options) {
|
|
|
871
1019
|
logger.error('Error: Project name is required');
|
|
872
1020
|
process.exit(1);
|
|
873
1021
|
}
|
|
874
|
-
const
|
|
875
|
-
const githubWorkflowsDir = resolve(projectDir, '.github', 'workflows');
|
|
1022
|
+
const githubWorkflowsDir = resolve(repoRoot, '.github', 'workflows');
|
|
876
1023
|
const githubActionPath = resolve(githubWorkflowsDir, 'screenci.yaml');
|
|
877
1024
|
const shouldAddGithubActionWorkflow = yes
|
|
878
1025
|
? true
|
|
@@ -889,10 +1036,13 @@ export async function runInit(projectNameArg, options) {
|
|
|
889
1036
|
const shouldInstallPlaywrightCli = yes
|
|
890
1037
|
? true
|
|
891
1038
|
: await promptInitPlaywrightCliSkillForPackageManager(packageManager, agent);
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1039
|
+
// The workflow lives at the repo root. If one already exists, skip it (do not
|
|
1040
|
+
// overwrite, do not fail) so re-running init stays non-destructive.
|
|
1041
|
+
const workflowAlreadyExists = shouldAddGithubActionWorkflow && existsSync(githubActionPath);
|
|
1042
|
+
if (workflowAlreadyExists) {
|
|
1043
|
+
logger.info(`Skipping GitHub Actions workflow: ${toWorkflowPath(relative(repoRoot, githubActionPath))} already exists`);
|
|
895
1044
|
}
|
|
1045
|
+
const shouldWriteGithubActionWorkflow = shouldAddGithubActionWorkflow && !workflowAlreadyExists;
|
|
896
1046
|
const skills = [];
|
|
897
1047
|
if (shouldInstallScreenCISkill) {
|
|
898
1048
|
skills.push('screenci');
|
|
@@ -905,61 +1055,99 @@ export async function runInit(projectNameArg, options) {
|
|
|
905
1055
|
? null
|
|
906
1056
|
: `${commands.skillsCommand} ${skillsArgs.join(' ')}`;
|
|
907
1057
|
const screenciDependency = getInitScreenciDependencyOverride() ?? (await readCurrentScreenciVersion());
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
await ensurePackageJsonTypeModule(packageJsonPath);
|
|
920
|
-
}
|
|
921
|
-
await writeInitGitignore(projectDir, packageManager);
|
|
922
|
-
await writeFile(resolve(projectDir, 'videos', 'example.video.ts'), generateExampleVideo());
|
|
923
|
-
if (shouldAddGithubActionWorkflow) {
|
|
924
|
-
await writeFile(githubActionPath, generateGithubAction(packageManager, isWorkspace));
|
|
925
|
-
}
|
|
926
|
-
if (packageManager === 'pnpm') {
|
|
927
|
-
await ensureSupportedPnpmVersion(projectDir);
|
|
928
|
-
}
|
|
929
|
-
if (packageManager === 'yarn') {
|
|
930
|
-
await writeFile(resolve(projectDir, '.yarnrc.yml'), 'nodeLinker: node-modules\n');
|
|
931
|
-
}
|
|
932
|
-
if (skillsArgs !== null) {
|
|
933
|
-
if (verbose) {
|
|
934
|
-
logger.info(`Running '${skillsCommand}'...`);
|
|
935
|
-
await spawnInherited(commands.skillsCommand, skillsArgs, projectDir, 'screenci init');
|
|
1058
|
+
// Everything below creates files / runs installs. If anything fails (or the
|
|
1059
|
+
// user interrupts), roll back the `screenci/` directory we created so the
|
|
1060
|
+
// next `init` run starts from a clean slate. Pre-existing repo-root files
|
|
1061
|
+
// (e.g. .github/, .claude/) are left untouched.
|
|
1062
|
+
let islandCreated = false;
|
|
1063
|
+
let scaffoldComplete = false;
|
|
1064
|
+
const removePartialIsland = () => {
|
|
1065
|
+
if (!islandCreated || scaffoldComplete)
|
|
1066
|
+
return;
|
|
1067
|
+
try {
|
|
1068
|
+
rmSync(islandDir, { recursive: true, force: true });
|
|
936
1069
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1070
|
+
catch {
|
|
1071
|
+
// best-effort cleanup
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
const onSigint = () => {
|
|
1075
|
+
removePartialIsland();
|
|
1076
|
+
process.exit(130);
|
|
1077
|
+
};
|
|
1078
|
+
const onSigterm = () => {
|
|
1079
|
+
removePartialIsland();
|
|
1080
|
+
process.exit(143);
|
|
1081
|
+
};
|
|
1082
|
+
process.on('SIGINT', onSigint);
|
|
1083
|
+
process.on('SIGTERM', onSigterm);
|
|
1084
|
+
process.on('exit', removePartialIsland);
|
|
1085
|
+
try {
|
|
1086
|
+
await mkdir(resolve(islandDir, 'videos'), { recursive: true });
|
|
1087
|
+
islandCreated = true;
|
|
1088
|
+
await writeFile(resolve(islandDir, 'screenci.config.ts'), generateConfig(projectName));
|
|
1089
|
+
await writeFile(resolve(islandDir, 'package.json'), generateIslandPackageJson(projectName));
|
|
1090
|
+
await writeFile(resolve(islandDir, 'tsconfig.json'), generateIslandTsconfig());
|
|
1091
|
+
await writeFile(resolve(islandDir, 'README.md'), generateIslandReadme(projectName, packageManager));
|
|
1092
|
+
await writeInitGitignore(islandDir, packageManager);
|
|
1093
|
+
await writeFile(resolve(islandDir, 'videos', 'example.video.ts'), generateExampleVideo());
|
|
1094
|
+
if (packageManager === 'pnpm') {
|
|
1095
|
+
// Resolve (and gate on) the pnpm version before writing the workspace
|
|
1096
|
+
// file so the build-approval key matches the installed pnpm.
|
|
1097
|
+
const pnpmVersionSupport = await ensureSupportedPnpmVersion(islandDir);
|
|
1098
|
+
await writeFile(resolve(islandDir, 'pnpm-workspace.yaml'), generatePnpmWorkspaceYaml(pnpmMajorFromSupport(pnpmVersionSupport)));
|
|
1099
|
+
}
|
|
1100
|
+
if (packageManager === 'yarn') {
|
|
1101
|
+
await writeFile(resolve(islandDir, '.yarnrc.yml'), 'nodeLinker: node-modules\n');
|
|
1102
|
+
}
|
|
1103
|
+
if (shouldWriteGithubActionWorkflow) {
|
|
1104
|
+
await mkdir(githubWorkflowsDir, { recursive: true });
|
|
1105
|
+
await writeFile(githubActionPath, generateGithubAction(packageManager, islandWorkflowPath));
|
|
1106
|
+
}
|
|
1107
|
+
// Install skills at the repo root so coding agents discover them when the
|
|
1108
|
+
// repository is opened as the workspace.
|
|
1109
|
+
if (skillsArgs !== null) {
|
|
1110
|
+
if (verbose) {
|
|
1111
|
+
logger.info(`Running '${skillsCommand}'...`);
|
|
1112
|
+
await spawnInherited(commands.skillsCommand, skillsArgs, repoRoot, 'screenci init');
|
|
942
1113
|
}
|
|
943
|
-
|
|
944
|
-
spinner
|
|
945
|
-
|
|
1114
|
+
else {
|
|
1115
|
+
const spinner = ora('Adding selected AI skills...').start();
|
|
1116
|
+
try {
|
|
1117
|
+
await spawnSilent(commands.skillsCommand, skillsArgs, repoRoot);
|
|
1118
|
+
spinner.succeed('Installing selected AI skills');
|
|
1119
|
+
}
|
|
1120
|
+
catch (err) {
|
|
1121
|
+
spinner.fail('AI skills install failed');
|
|
1122
|
+
throw err;
|
|
1123
|
+
}
|
|
946
1124
|
}
|
|
947
1125
|
}
|
|
1126
|
+
await installInitDependencies(islandDir, verbose, screenciDependency, shouldInstallPlaywrightCli, commands);
|
|
1127
|
+
if (shouldInstallPlaywrightBrowsers) {
|
|
1128
|
+
logger.info(`Installing Playwright Chromium headless shell with '${commands.playwrightRun} install --only-shell chromium'...`);
|
|
1129
|
+
const [browserCmd, ...browserArgs] = buildPlaywrightSpawnArgs(packageManager, 'install', '--only-shell', 'chromium');
|
|
1130
|
+
await spawnInherited(browserCmd, browserArgs, islandDir, 'screenci init');
|
|
1131
|
+
logger.info(`${pc.green('✔')} Playwright Chromium headless shell installed successfully`);
|
|
1132
|
+
}
|
|
1133
|
+
if (shouldInstallPlaywrightOsDependencies) {
|
|
1134
|
+
logger.info(`Installing Playwright operating system dependencies with '${commands.playwrightRun} install-deps chromium'...`);
|
|
1135
|
+
const [depsCmd, ...depsArgs] = buildPlaywrightSpawnArgs(packageManager, 'install-deps', 'chromium');
|
|
1136
|
+
await spawnInherited(depsCmd, depsArgs, islandDir, 'screenci init');
|
|
1137
|
+
logger.info(`${pc.green('✔')} Playwright operating system dependencies installed successfully`);
|
|
1138
|
+
}
|
|
1139
|
+
scaffoldComplete = true;
|
|
948
1140
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const [browserCmd, ...browserArgs] = buildPlaywrightSpawnArgs(packageManager, 'install', '--only-shell', 'chromium');
|
|
953
|
-
await spawnInherited(browserCmd, browserArgs, projectDir, 'screenci init');
|
|
954
|
-
logger.info(`${pc.green('✔')} Playwright Chromium headless shell installed successfully`);
|
|
1141
|
+
catch (err) {
|
|
1142
|
+
removePartialIsland();
|
|
1143
|
+
throw err;
|
|
955
1144
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
logger.info(`${pc.green('✔')} Playwright operating system dependencies installed successfully`);
|
|
1145
|
+
finally {
|
|
1146
|
+
process.off('SIGINT', onSigint);
|
|
1147
|
+
process.off('SIGTERM', onSigterm);
|
|
1148
|
+
process.off('exit', removePartialIsland);
|
|
961
1149
|
}
|
|
962
|
-
printInitNextSteps(
|
|
1150
|
+
printInitNextSteps(islandDir, islandDirName, packageManager);
|
|
963
1151
|
}
|
|
964
1152
|
function handleCreateCommanderError(err) {
|
|
965
1153
|
if (!(err instanceof CommanderError)) {
|