screenci 0.0.63 → 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/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
- // Filesystem detection is only performed during init (when cwd is provided).
56
- // record/test commands rely solely on the user agent, which correctly reflects
57
- // how the CLI was invoked (e.g. "pnpm exec screenci record" sets the agent).
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
- function detectYarnWorkspace(cwd) {
81
- try {
82
- const raw = readFileSync(resolve(cwd, 'package.json'), 'utf-8');
83
- const pkg = JSON.parse(raw);
84
- return pkg.workspaces !== undefined;
85
- }
86
- catch {
87
- return false;
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: (pkg) => ['add', '--save-dev', ...workspaceFlag, pkg],
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: (pkg) => ['add', '--dev', ...workspaceFlag, pkg],
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: (pkg) => ['install', '--save-dev', pkg],
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
- function generateEmptyPackageJson() {
409
- return '{\n "type": "module"\n}\n';
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
- async function ensurePackageJsonTypeModule(packageJsonPath) {
412
- try {
413
- const raw = await readFile(packageJsonPath, 'utf-8');
414
- const pkg = JSON.parse(raw);
415
- if (pkg['type'] === 'module')
416
- return;
417
- pkg['type'] = 'module';
418
- await writeFile(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
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
- catch {
421
- // Malformed or unreadable package.json — leave it untouched.
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 Playwright Test...',
648
- args: commands.installArgs(`@playwright/test@${PLAYWRIGHT_TEST_VERSION}`),
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('Inside that directory, you can run several commands:');
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.');
@@ -698,18 +828,23 @@ function printInitNextSteps(projectDir, packageManager) {
698
828
  logger.info('');
699
829
  logger.info('We suggest that you begin by typing:');
700
830
  logger.info('');
831
+ logger.info(` ${pc.cyan(`cd ${islandDirName}`)}`);
701
832
  logger.info(` ${pc.cyan(`${commands.screenciRun} test`)}`);
702
833
  logger.info('');
703
834
  logger.info('And check out the following files:');
704
- logger.info(' - ./videos/example.video.ts - Example video script');
705
- logger.info(' - ./screenci.config.ts - ScreenCI configuration');
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`);
706
838
  logger.info('');
707
839
  logger.info(`Visit ${pc.cyan('https://screenci.com/docs')} for more information.`);
708
840
  logger.info('');
709
841
  logger.info('Happy hacking! 🎥');
710
842
  }
711
- function generateGithubAction(packageManager, isWorkspace = false) {
712
- const commands = getPackageManagerCommand(packageManager, isWorkspace);
843
+ function generateGithubAction(packageManager, islandWorkflowPath) {
844
+ const commands = getPackageManagerCommand(packageManager);
845
+ const cacheDependencyPath = islandWorkflowPath === '.'
846
+ ? commands.lockfileName
847
+ : `${islandWorkflowPath}/${commands.lockfileName}`;
713
848
  return `name: ScreenCI
714
849
 
715
850
  on:
@@ -739,22 +874,22 @@ jobs:
739
874
  with:
740
875
  node-version: 24
741
876
  cache: ${commands.cacheName}
742
- cache-dependency-path: ${commands.lockfileName}
877
+ cache-dependency-path: ${cacheDependencyPath}
743
878
 
744
879
  - name: Install dependencies
745
- working-directory: .
880
+ working-directory: ${islandWorkflowPath}
746
881
  env:
747
882
  HUSKY: 0
748
883
  npm_config_strict_dep_builds: false
749
884
  run: ${commands.frozenInstallCommand}
750
885
 
751
886
  - name: Install Chromium Headless Shell
752
- working-directory: .
887
+ working-directory: ${islandWorkflowPath}
753
888
  run: ${commands.playwrightRun} install --only-shell chromium
754
889
 
755
890
  - id: record
756
891
  name: Record
757
- working-directory: .
892
+ working-directory: ${islandWorkflowPath}
758
893
  env:
759
894
  SCREENCI_SECRET: \${{ secrets.SCREENCI_SECRET }}
760
895
  run: ${commands.screenciRun} record
@@ -854,12 +989,20 @@ function getInitScreenciDependencyOverride() {
854
989
  export async function runInit(projectNameArg, options) {
855
990
  const { verbose, yes, agent, packageManager } = options;
856
991
  const initCwd = getInitProjectRoot();
857
- const isWorkspace = packageManager === 'pnpm'
858
- ? detectPnpmWorkspace(initCwd)
859
- : packageManager === 'yarn'
860
- ? detectYarnWorkspace(initCwd)
861
- : false;
862
- const commands = getPackageManagerCommand(packageManager, isWorkspace);
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
+ }
863
1006
  if (packageManager === 'yarn') {
864
1007
  await ensureSupportedYarnVersion(initCwd);
865
1008
  }
@@ -871,8 +1014,7 @@ export async function runInit(projectNameArg, options) {
871
1014
  logger.error('Error: Project name is required');
872
1015
  process.exit(1);
873
1016
  }
874
- const projectDir = initCwd;
875
- const githubWorkflowsDir = resolve(projectDir, '.github', 'workflows');
1017
+ const githubWorkflowsDir = resolve(repoRoot, '.github', 'workflows');
876
1018
  const githubActionPath = resolve(githubWorkflowsDir, 'screenci.yaml');
877
1019
  const shouldAddGithubActionWorkflow = yes
878
1020
  ? true
@@ -889,10 +1031,13 @@ export async function runInit(projectNameArg, options) {
889
1031
  const shouldInstallPlaywrightCli = yes
890
1032
  ? true
891
1033
  : await promptInitPlaywrightCliSkillForPackageManager(packageManager, agent);
892
- if (shouldAddGithubActionWorkflow && existsSync(githubActionPath)) {
893
- logger.error('Error: GitHub Actions workflow ".github/workflows/screenci.yaml" already exists');
894
- process.exit(1);
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`);
895
1039
  }
1040
+ const shouldWriteGithubActionWorkflow = shouldAddGithubActionWorkflow && !workflowAlreadyExists;
896
1041
  const skills = [];
897
1042
  if (shouldInstallScreenCISkill) {
898
1043
  skills.push('screenci');
@@ -905,61 +1050,99 @@ export async function runInit(projectNameArg, options) {
905
1050
  ? null
906
1051
  : `${commands.skillsCommand} ${skillsArgs.join(' ')}`;
907
1052
  const screenciDependency = getInitScreenciDependencyOverride() ?? (await readCurrentScreenciVersion());
908
- const packageJsonPath = resolve(projectDir, 'package.json');
909
- const hasExistingPackageJson = existsSync(packageJsonPath);
910
- await mkdir(resolve(projectDir, 'videos'), { recursive: true });
911
- if (shouldAddGithubActionWorkflow) {
912
- await mkdir(githubWorkflowsDir, { recursive: true });
913
- }
914
- await writeFile(resolve(projectDir, 'screenci.config.ts'), generateConfig(projectName));
915
- if (!hasExistingPackageJson) {
916
- await writeFile(packageJsonPath, generateEmptyPackageJson());
917
- }
918
- else {
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');
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 });
936
1064
  }
937
- else {
938
- const spinner = ora('Adding selected AI skills...').start();
939
- try {
940
- await spawnSilent(commands.skillsCommand, skillsArgs, projectDir);
941
- spinner.succeed('Installing selected AI skills');
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');
942
1108
  }
943
- catch (err) {
944
- spinner.fail('AI skills install failed');
945
- throw err;
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
+ }
946
1119
  }
947
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;
948
1135
  }
949
- await installInitDependencies(projectDir, verbose, screenciDependency, shouldInstallPlaywrightCli, commands);
950
- if (shouldInstallPlaywrightBrowsers) {
951
- logger.info(`Installing Playwright Chromium headless shell with '${commands.playwrightRun} install --only-shell chromium'...`);
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`);
1136
+ catch (err) {
1137
+ removePartialIsland();
1138
+ throw err;
955
1139
  }
956
- if (shouldInstallPlaywrightOsDependencies) {
957
- logger.info(`Installing Playwright operating system dependencies with '${commands.playwrightRun} install-deps chromium'...`);
958
- const [depsCmd, ...depsArgs] = buildPlaywrightSpawnArgs(packageManager, 'install-deps', 'chromium');
959
- await spawnInherited(depsCmd, depsArgs, projectDir, 'screenci init');
960
- 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);
961
1144
  }
962
- printInitNextSteps(projectDir, packageManager);
1145
+ printInitNextSteps(islandDir, islandDirName, packageManager);
963
1146
  }
964
1147
  function handleCreateCommanderError(err) {
965
1148
  if (!(err instanceof CommanderError)) {