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/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,117 @@ 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
+ // `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
- catch {
421
- // Malformed or unreadable package.json — leave it untouched.
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 Playwright Test...',
648
- args: commands.installArgs(`@playwright/test@${PLAYWRIGHT_TEST_VERSION}`),
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('Inside that directory, you can run several commands:');
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(' - ./videos/example.video.ts - Example video script');
705
- logger.info(' - ./screenci.config.ts - ScreenCI configuration');
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, isWorkspace = false) {
712
- const commands = getPackageManagerCommand(packageManager, isWorkspace);
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: ${commands.lockfileName}
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 isWorkspace = packageManager === 'pnpm'
858
- ? detectPnpmWorkspace(initCwd)
859
- : packageManager === 'yarn'
860
- ? detectYarnWorkspace(initCwd)
861
- : false;
862
- const commands = getPackageManagerCommand(packageManager, isWorkspace);
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 projectDir = initCwd;
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
- if (shouldAddGithubActionWorkflow && existsSync(githubActionPath)) {
893
- logger.error('Error: GitHub Actions workflow ".github/workflows/screenci.yaml" already exists');
894
- process.exit(1);
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
- 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');
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
- 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');
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
- catch (err) {
944
- spinner.fail('AI skills install failed');
945
- throw err;
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
- 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`);
1141
+ catch (err) {
1142
+ removePartialIsland();
1143
+ throw err;
955
1144
  }
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`);
1145
+ finally {
1146
+ process.off('SIGINT', onSigint);
1147
+ process.off('SIGTERM', onSigterm);
1148
+ process.off('exit', removePartialIsland);
961
1149
  }
962
- printInitNextSteps(projectDir, packageManager);
1150
+ printInitNextSteps(islandDir, islandDirName, packageManager);
963
1151
  }
964
1152
  function handleCreateCommanderError(err) {
965
1153
  if (!(err instanceof CommanderError)) {