@i-santos/create-package-starter 1.5.0-beta.2 → 1.5.0-beta.4

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 CHANGED
@@ -8,6 +8,7 @@ Scaffold and standardize npm packages with a Changesets-first release workflow.
8
8
  npx @i-santos/create-package-starter --name hello-package
9
9
  npx @i-santos/create-package-starter --name @i-santos/swarm --default-branch main
10
10
  npx @i-santos/create-package-starter init --dir ./existing-package
11
+ npx @i-santos/create-package-starter init --dir . --with-github --with-beta --with-npm --yes
11
12
  npx @i-santos/create-package-starter setup-github --repo i-santos/firestack --dry-run
12
13
  npx @i-santos/create-package-starter setup-beta --dir . --beta-branch release/beta
13
14
  npx @i-santos/create-package-starter promote-stable --dir . --type patch --summary "Promote beta to stable"
@@ -30,6 +31,14 @@ Bootstrap existing package:
30
31
  - `--cleanup-legacy-release` (remove `release:beta*`, `release:stable*`, `release:promote*`, `release:rollback*`, `release:dist-tags`)
31
32
  - `--scope <scope>` (optional placeholder helper for docs/templates)
32
33
  - `--default-branch <branch>` (default: `main`)
34
+ - `--beta-branch <branch>` (default: `release/beta`)
35
+ - `--with-github` (run GitHub setup in same flow)
36
+ - `--with-npm` (run npm setup in same flow)
37
+ - `--with-beta` (run beta flow setup; implies `--with-github`)
38
+ - `--repo <owner/repo>` (optional; inferred from `remote.origin.url` when omitted)
39
+ - `--ruleset <path>` (optional JSON override for main ruleset payload)
40
+ - `--dry-run` (preview planned operations without mutating)
41
+ - `--yes` (skip confirmation prompts)
33
42
 
34
43
  Configure GitHub repository settings:
35
44
 
@@ -88,6 +97,11 @@ The generated and managed baseline includes:
88
97
  - Existing custom `check` script is preserved unless `--force`.
89
98
  - Existing `@changesets/cli` version is preserved unless `--force`.
90
99
  - Lowercase `.github/pull_request_template.md` is recognized as an existing equivalent template.
100
+ - If no `--with-*` flags are provided:
101
+ - TTY: asks interactively which external setup to run (`github`, `npm`, `beta`).
102
+ - non-TTY: runs local init only and prints warning with next steps.
103
+ - Integrated mode (`--with-github/--with-npm/--with-beta`) pre-validates everything first (gh auth, npm auth, repo/branch/ruleset/package checks) and fails fast before local mutations if validation fails.
104
+ - Integrated mode asks confirmation for sensitive external operations and ruleset/branch adoption conflicts (unless `--yes`).
91
105
 
92
106
  ## Output Summary Contract
93
107
 
@@ -122,6 +136,7 @@ If `gh` is missing or unauthenticated, command exits non-zero with actionable gu
122
136
  - creates/preserves `.github/workflows/ci.yml` with beta+stable branch triggers
123
137
  - ensures `release/beta` branch exists remotely (created from default branch if missing)
124
138
  - applies beta branch protection ruleset on GitHub (including required CI matrix checks for Node 18 and 20)
139
+ - applies beta branch protection ruleset on GitHub with stable required check context (`required-check`)
125
140
  - asks for confirmation before mutating repository settings and again before overwriting existing beta ruleset
126
141
  - supports safe-merge by default and `--force` overwrite
127
142
  - supports configurable beta branch (`release/beta` by default)
@@ -147,6 +162,8 @@ If `gh` is missing or unauthenticated, command exits non-zero with actionable gu
147
162
 
148
163
  Important: Trusted Publisher still needs manual setup in npm package settings.
149
164
 
165
+ When npm setup runs inside orchestrated `init --with-npm`, first publish is automatic when package is not found on npm.
166
+
150
167
  ## Trusted Publishing Note
151
168
 
152
169
  If package does not exist on npm yet, first publish may be manual:
package/lib/run.js CHANGED
@@ -6,7 +6,9 @@ const readline = require('readline/promises');
6
6
  const CHANGESETS_DEP = '@changesets/cli';
7
7
  const CHANGESETS_DEP_VERSION = '^2.29.7';
8
8
  const DEFAULT_BASE_BRANCH = 'main';
9
+ const DEFAULT_BETA_BRANCH = 'release/beta';
9
10
  const DEFAULT_RULESET_NAME = 'Default main branch protection';
11
+ const REQUIRED_CHECK_CONTEXT = 'required-check';
10
12
 
11
13
  const MANAGED_FILE_SPECS = [
12
14
  ['.changeset/config.json', '.changeset/config.json'],
@@ -17,14 +19,14 @@ const MANAGED_FILE_SPECS = [
17
19
  ['.github/CODEOWNERS', '.github/CODEOWNERS'],
18
20
  ['CONTRIBUTING.md', 'CONTRIBUTING.md'],
19
21
  ['README.md', 'README.md'],
20
- ['.gitignore', '.gitignore']
22
+ ['.gitignore', 'gitignore']
21
23
  ];
22
24
 
23
25
  function usage() {
24
26
  return [
25
27
  'Usage:',
26
28
  ' create-package-starter --name <name> [--out <directory>] [--default-branch <branch>]',
27
- ' create-package-starter init [--dir <directory>] [--force] [--cleanup-legacy-release] [--scope <scope>] [--default-branch <branch>]',
29
+ ' create-package-starter init [--dir <directory>] [--force] [--cleanup-legacy-release] [--scope <scope>] [--default-branch <branch>] [--with-github] [--with-npm] [--with-beta] [--repo <owner/repo>] [--beta-branch <branch>] [--ruleset <path>] [--dry-run] [--yes]',
28
30
  ' create-package-starter setup-github [--repo <owner/repo>] [--default-branch <branch>] [--ruleset <path>] [--dry-run]',
29
31
  ' create-package-starter setup-beta [--dir <directory>] [--repo <owner/repo>] [--beta-branch <branch>] [--default-branch <branch>] [--force] [--dry-run] [--yes]',
30
32
  ' create-package-starter promote-stable [--dir <directory>] [--type patch|minor|major] [--summary <text>] [--dry-run]',
@@ -36,6 +38,7 @@ function usage() {
36
38
  ' create-package-starter init --dir ./my-package',
37
39
  ' create-package-starter init --cleanup-legacy-release',
38
40
  ' create-package-starter setup-github --repo i-santos/firestack --dry-run',
41
+ ' create-package-starter init --dir . --with-github --with-beta --with-npm --yes',
39
42
  ' create-package-starter setup-beta --dir . --beta-branch release/beta',
40
43
  ' create-package-starter promote-stable --dir . --type patch --summary "Promote beta to stable"',
41
44
  ' create-package-starter setup-npm --dir . --publish-first'
@@ -95,7 +98,15 @@ function parseInitArgs(argv) {
95
98
  force: false,
96
99
  cleanupLegacyRelease: false,
97
100
  defaultBranch: DEFAULT_BASE_BRANCH,
98
- scope: ''
101
+ betaBranch: DEFAULT_BETA_BRANCH,
102
+ scope: '',
103
+ repo: '',
104
+ ruleset: '',
105
+ withGithub: false,
106
+ withNpm: false,
107
+ withBeta: false,
108
+ dryRun: false,
109
+ yes: false
99
110
  };
100
111
 
101
112
  for (let i = 0; i < argv.length; i += 1) {
@@ -119,6 +130,49 @@ function parseInitArgs(argv) {
119
130
  continue;
120
131
  }
121
132
 
133
+ if (token === '--beta-branch') {
134
+ args.betaBranch = parseValueFlag(argv, i, '--beta-branch');
135
+ i += 1;
136
+ continue;
137
+ }
138
+
139
+ if (token === '--repo') {
140
+ args.repo = parseValueFlag(argv, i, '--repo');
141
+ i += 1;
142
+ continue;
143
+ }
144
+
145
+ if (token === '--ruleset') {
146
+ args.ruleset = parseValueFlag(argv, i, '--ruleset');
147
+ i += 1;
148
+ continue;
149
+ }
150
+
151
+ if (token === '--with-github') {
152
+ args.withGithub = true;
153
+ continue;
154
+ }
155
+
156
+ if (token === '--with-npm') {
157
+ args.withNpm = true;
158
+ continue;
159
+ }
160
+
161
+ if (token === '--with-beta') {
162
+ args.withBeta = true;
163
+ continue;
164
+ }
165
+
166
+ if (token === '--dry-run') {
167
+ args.dryRun = true;
168
+ continue;
169
+ }
170
+
171
+ if (token === '--yes') {
172
+ args.yes = true;
173
+ continue;
174
+ }
175
+
122
176
  if (token === '--force') {
123
177
  args.force = true;
124
178
  continue;
@@ -223,7 +277,7 @@ function parseSetupNpmArgs(argv) {
223
277
  function parseSetupBetaArgs(argv) {
224
278
  const args = {
225
279
  dir: process.cwd(),
226
- betaBranch: 'release/beta',
280
+ betaBranch: DEFAULT_BETA_BRANCH,
227
281
  defaultBranch: DEFAULT_BASE_BRANCH,
228
282
  force: false,
229
283
  yes: false,
@@ -419,8 +473,11 @@ function copyDirRecursive(sourceDir, targetDir, variables, relativeBase = '') {
419
473
 
420
474
  for (const entry of entries) {
421
475
  const srcPath = path.join(sourceDir, entry.name);
422
- const destPath = path.join(targetDir, entry.name);
423
- const relativePath = path.posix.join(relativeBase, entry.name);
476
+ const destinationEntryName = relativeBase === '' && entry.name === 'gitignore'
477
+ ? '.gitignore'
478
+ : entry.name;
479
+ const destPath = path.join(targetDir, destinationEntryName);
480
+ const relativePath = path.posix.join(relativeBase, destinationEntryName);
424
481
 
425
482
  if (entry.isDirectory()) {
426
483
  createdFiles.push(...copyDirRecursive(srcPath, destPath, variables, relativePath));
@@ -485,6 +542,74 @@ function printSummary(title, summary) {
485
542
  console.log(`warnings: ${list(summary.warnings)}`);
486
543
  }
487
544
 
545
+ class StepReporter {
546
+ constructor() {
547
+ this.active = null;
548
+ this.frames = ['-', '\\', '|', '/'];
549
+ this.frameIndex = 0;
550
+ }
551
+
552
+ canSpin() {
553
+ return Boolean(process.stdout.isTTY) && process.env.CI !== 'true';
554
+ }
555
+
556
+ start(stepId, message) {
557
+ this.stop();
558
+ if (!this.canSpin()) {
559
+ logStep('run', message);
560
+ return;
561
+ }
562
+
563
+ this.active = {
564
+ id: stepId,
565
+ message,
566
+ timer: setInterval(() => {
567
+ const frame = this.frames[this.frameIndex % this.frames.length];
568
+ this.frameIndex += 1;
569
+ process.stdout.write(`\r${frame} ${message}`);
570
+ }, 80)
571
+ };
572
+ }
573
+
574
+ end(status, message) {
575
+ if (this.active && this.active.timer) {
576
+ clearInterval(this.active.timer);
577
+ const finalLabel = status === 'ok'
578
+ ? 'OK'
579
+ : status === 'warn'
580
+ ? 'WARN'
581
+ : 'ERR';
582
+ process.stdout.write(`\r[${finalLabel}] ${message}\n`);
583
+ this.active = null;
584
+ return;
585
+ }
586
+
587
+ logStep(status, message);
588
+ }
589
+
590
+ ok(stepId, message) {
591
+ this.end('ok', message);
592
+ }
593
+
594
+ warn(stepId, message) {
595
+ this.end('warn', message);
596
+ }
597
+
598
+ fail(stepId, message) {
599
+ this.end('err', message);
600
+ }
601
+
602
+ stop() {
603
+ if (!this.active || !this.active.timer) {
604
+ return;
605
+ }
606
+
607
+ clearInterval(this.active.timer);
608
+ process.stdout.write('\r');
609
+ this.active = null;
610
+ }
611
+ }
612
+
488
613
  function logStep(status, message) {
489
614
  const labels = {
490
615
  run: '[RUN ]',
@@ -517,6 +642,43 @@ async function confirmOrThrow(questionText) {
517
642
  }
518
643
  }
519
644
 
645
+ async function askYesNo(questionText, defaultValue = false) {
646
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
647
+ return defaultValue;
648
+ }
649
+
650
+ const rl = readline.createInterface({
651
+ input: process.stdin,
652
+ output: process.stdout
653
+ });
654
+
655
+ try {
656
+ const suffix = defaultValue ? '[Y/n]' : '[y/N]';
657
+ const answer = await rl.question(`${questionText} ${suffix} `);
658
+ const normalized = answer.trim().toLowerCase();
659
+
660
+ if (!normalized) {
661
+ return defaultValue;
662
+ }
663
+
664
+ return normalized === 'y' || normalized === 'yes';
665
+ } finally {
666
+ rl.close();
667
+ }
668
+ }
669
+
670
+ function mergeSummary(target, source) {
671
+ target.createdFiles.push(...source.createdFiles);
672
+ target.overwrittenFiles.push(...source.overwrittenFiles);
673
+ target.skippedFiles.push(...source.skippedFiles);
674
+ target.updatedScriptKeys.push(...source.updatedScriptKeys);
675
+ target.skippedScriptKeys.push(...source.skippedScriptKeys);
676
+ target.removedScriptKeys.push(...source.removedScriptKeys);
677
+ target.updatedDependencyKeys.push(...source.updatedDependencyKeys);
678
+ target.skippedDependencyKeys.push(...source.skippedDependencyKeys);
679
+ target.warnings.push(...source.warnings);
680
+ }
681
+
520
682
  function ensureFileFromTemplate(targetPath, templatePath, options) {
521
683
  const exists = fs.existsSync(targetPath);
522
684
 
@@ -524,6 +686,10 @@ function ensureFileFromTemplate(targetPath, templatePath, options) {
524
686
  return 'skipped';
525
687
  }
526
688
 
689
+ if (options.dryRun) {
690
+ return exists ? 'overwritten' : 'created';
691
+ }
692
+
527
693
  const source = fs.readFileSync(templatePath, 'utf8');
528
694
  const rendered = renderTemplateString(source, options.variables);
529
695
 
@@ -627,6 +793,7 @@ function upsertReleaseWorkflow(targetPath, templatePath, options) {
627
793
 
628
794
  const result = ensureFileFromTemplate(targetPath, templatePath, {
629
795
  force: options.force,
796
+ dryRun: options.dryRun,
630
797
  variables: options.variables
631
798
  });
632
799
  return { result };
@@ -804,15 +971,16 @@ function configureExistingPackage(packageDir, templateDir, options) {
804
971
 
805
972
  updateManagedFiles(packageDir, templateDir, {
806
973
  force: options.force,
974
+ dryRun: options.dryRun,
807
975
  variables: {
808
976
  PACKAGE_NAME: packageName,
809
977
  DEFAULT_BRANCH: options.defaultBranch,
810
- BETA_BRANCH: options.betaBranch || 'release/beta',
978
+ BETA_BRANCH: options.betaBranch || DEFAULT_BETA_BRANCH,
811
979
  SCOPE: deriveScope(options.scope, packageName)
812
980
  }
813
981
  }, summary);
814
982
 
815
- if (packageJsonChanged) {
983
+ if (packageJsonChanged && !options.dryRun) {
816
984
  writeJsonFile(packageJsonPath, packageJson);
817
985
  }
818
986
 
@@ -843,7 +1011,7 @@ function createNewPackage(args) {
843
1011
  const createdFiles = copyDirRecursive(templateDir, targetDir, {
844
1012
  PACKAGE_NAME: args.name,
845
1013
  DEFAULT_BRANCH: args.defaultBranch,
846
- BETA_BRANCH: 'release/beta',
1014
+ BETA_BRANCH: DEFAULT_BETA_BRANCH,
847
1015
  SCOPE: deriveScope('', args.name)
848
1016
  });
849
1017
 
@@ -856,13 +1024,100 @@ function createNewPackage(args) {
856
1024
  printSummary(`Package created in ${targetDir}`, summary);
857
1025
  }
858
1026
 
859
- function initExistingPackage(args) {
1027
+ async function initExistingPackage(args, dependencies = {}) {
1028
+ const reporter = new StepReporter();
1029
+ const selections = await resolveInitSelections(args);
860
1030
  const packageRoot = path.resolve(__dirname, '..');
861
1031
  const templateDir = path.join(packageRoot, 'template');
862
1032
  const targetDir = path.resolve(args.dir);
1033
+ const overallSummary = createSummary();
1034
+ const deps = {
1035
+ exec: dependencies.exec || execCommand
1036
+ };
1037
+
1038
+ if (!selections.withGithub && !selections.withNpm && !selections.withBeta && !process.stdin.isTTY) {
1039
+ overallSummary.warnings.push('No --with-* flags were provided in non-interactive mode. Only local init was applied.');
1040
+ }
1041
+
1042
+ const context = prevalidateInitExecution(args, selections, dependencies, reporter);
1043
+ await confirmInitPlan(args, selections, context, overallSummary);
863
1044
 
864
- const summary = configureExistingPackage(targetDir, templateDir, args);
865
- printSummary(`Project initialized in ${targetDir}`, summary);
1045
+ reporter.start('local-init', 'Applying local package bootstrap...');
1046
+ const localSummary = configureExistingPackage(targetDir, templateDir, {
1047
+ ...args,
1048
+ dryRun: args.dryRun,
1049
+ betaBranch: args.betaBranch
1050
+ });
1051
+ mergeSummary(overallSummary, localSummary);
1052
+ reporter.ok('local-init', args.dryRun ? 'Local package bootstrap previewed.' : 'Local package bootstrap applied.');
1053
+
1054
+ if (selections.withGithub && selections.withBeta) {
1055
+ ensureBetaWorkflowTriggers(
1056
+ targetDir,
1057
+ templateDir,
1058
+ {
1059
+ force: args.force,
1060
+ dryRun: args.dryRun,
1061
+ defaultBranch: args.defaultBranch,
1062
+ betaBranch: args.betaBranch,
1063
+ packageName: context.packageName,
1064
+ scope: deriveScope(args.scope, context.packageName)
1065
+ },
1066
+ overallSummary,
1067
+ reporter
1068
+ );
1069
+ }
1070
+
1071
+ let repo = context.repo;
1072
+ if (selections.withGithub) {
1073
+ const githubSummary = createSummary();
1074
+ const mainResult = applyGithubMainSetup(
1075
+ {
1076
+ repo: context.repo,
1077
+ defaultBranch: args.defaultBranch,
1078
+ ruleset: args.ruleset,
1079
+ dryRun: args.dryRun
1080
+ },
1081
+ { exec: deps.exec },
1082
+ githubSummary,
1083
+ reporter
1084
+ );
1085
+ repo = mainResult.repo;
1086
+
1087
+ if (selections.withBeta) {
1088
+ applyGithubBetaSetup(
1089
+ {
1090
+ betaBranch: args.betaBranch,
1091
+ defaultBranch: args.defaultBranch,
1092
+ dryRun: args.dryRun
1093
+ },
1094
+ { exec: deps.exec },
1095
+ githubSummary,
1096
+ reporter,
1097
+ repo
1098
+ );
1099
+ }
1100
+
1101
+ mergeSummary(overallSummary, githubSummary);
1102
+ }
1103
+
1104
+ if (selections.withNpm) {
1105
+ const npmSummary = runNpmSetup(
1106
+ {
1107
+ dir: targetDir,
1108
+ dryRun: args.dryRun,
1109
+ publishFirst: false
1110
+ },
1111
+ { exec: deps.exec },
1112
+ {
1113
+ reporter,
1114
+ publishMissingByDefault: true
1115
+ }
1116
+ );
1117
+ mergeSummary(overallSummary, npmSummary);
1118
+ }
1119
+
1120
+ printSummary(`Project initialized in ${targetDir}`, overallSummary);
866
1121
  }
867
1122
 
868
1123
  function execCommand(command, args, options = {}) {
@@ -925,6 +1180,17 @@ function createBaseRulesetPayload(defaultBranch) {
925
1180
  require_last_push_approval: false,
926
1181
  required_review_thread_resolution: true
927
1182
  }
1183
+ },
1184
+ {
1185
+ type: 'required_status_checks',
1186
+ parameters: {
1187
+ strict_required_status_checks_policy: true,
1188
+ required_status_checks: [
1189
+ {
1190
+ context: REQUIRED_CHECK_CONTEXT
1191
+ }
1192
+ ]
1193
+ }
928
1194
  }
929
1195
  ]
930
1196
  };
@@ -961,10 +1227,7 @@ function createBetaRulesetPayload(betaBranch) {
961
1227
  strict_required_status_checks_policy: true,
962
1228
  required_status_checks: [
963
1229
  {
964
- context: 'CI / check (18) (pull_request)'
965
- },
966
- {
967
- context: 'CI / check (20) (pull_request)'
1230
+ context: REQUIRED_CHECK_CONTEXT
968
1231
  }
969
1232
  ]
970
1233
  }
@@ -1127,6 +1390,15 @@ function findRulesetByName(deps, repo, name) {
1127
1390
  return rulesets.find((ruleset) => ruleset.name === name) || null;
1128
1391
  }
1129
1392
 
1393
+ function listRulesets(deps, repo) {
1394
+ const listResult = ghApi(deps, 'GET', `/repos/${repo}/rulesets`);
1395
+ if (listResult.status !== 0) {
1396
+ throw new Error(`Failed to list rulesets: ${listResult.stderr || listResult.stdout}`.trim());
1397
+ }
1398
+
1399
+ return parseJsonOutput(listResult.stdout || '[]', 'Failed to parse rulesets response from GitHub API.');
1400
+ }
1401
+
1130
1402
  function ensureNpmAvailable(deps) {
1131
1403
  const version = deps.exec('npm', ['--version']);
1132
1404
  if (version.status !== 0) {
@@ -1155,55 +1427,309 @@ function packageExistsOnNpm(deps, packageName) {
1155
1427
  throw new Error(`Failed to check package on npm: ${view.stderr || view.stdout}`.trim());
1156
1428
  }
1157
1429
 
1158
- function setupNpm(args, dependencies = {}) {
1430
+ async function resolveInitSelections(args) {
1431
+ const explicit = args.withGithub || args.withNpm || args.withBeta;
1432
+ const selected = {
1433
+ withGithub: args.withGithub,
1434
+ withNpm: args.withNpm,
1435
+ withBeta: args.withBeta
1436
+ };
1437
+
1438
+ if (!explicit) {
1439
+ if (process.stdin.isTTY && process.stdout.isTTY) {
1440
+ selected.withGithub = await askYesNo('Enable GitHub repository setup (rulesets/settings)?', false);
1441
+ selected.withNpm = await askYesNo('Enable npm setup (auth + package check + first publish if needed)?', false);
1442
+ selected.withBeta = selected.withGithub
1443
+ ? await askYesNo(`Enable beta flow setup using branch "${args.betaBranch}"?`, true)
1444
+ : false;
1445
+ } else {
1446
+ selected.withGithub = false;
1447
+ selected.withNpm = false;
1448
+ selected.withBeta = false;
1449
+ }
1450
+ }
1451
+
1452
+ if (selected.withBeta) {
1453
+ selected.withGithub = true;
1454
+ }
1455
+
1456
+ return selected;
1457
+ }
1458
+
1459
+ function summarizePlannedInitActions(selections, args, context) {
1460
+ const lines = [
1461
+ 'This init execution will apply:',
1462
+ '- local managed files/scripts/dependencies bootstrap'
1463
+ ];
1464
+
1465
+ if (selections.withGithub) {
1466
+ lines.push(`- GitHub main settings/ruleset for ${context.repo}`);
1467
+ }
1468
+ if (selections.withBeta) {
1469
+ lines.push(`- beta branch flow for ${args.betaBranch} (create branch if missing + ruleset + workflow triggers)`);
1470
+ }
1471
+ if (selections.withNpm) {
1472
+ if (context.existsOnNpm) {
1473
+ lines.push(`- npm setup for ${context.packageName} (already published; no first publish)`);
1474
+ } else {
1475
+ lines.push(`- npm setup for ${context.packageName} (first publish will run automatically)`);
1476
+ }
1477
+ }
1478
+
1479
+ return lines.join('\n');
1480
+ }
1481
+
1482
+ function upsertCiWorkflow(targetPath, templatePath, options) {
1483
+ return upsertReleaseWorkflow(targetPath, templatePath, options);
1484
+ }
1485
+
1486
+ function ensureBetaWorkflowTriggers(targetDir, templateDir, options, summary, reporter) {
1487
+ const workflowRelativePath = '.github/workflows/release.yml';
1488
+ const workflowTemplatePath = path.join(templateDir, workflowRelativePath);
1489
+ const workflowTargetPath = path.join(targetDir, workflowRelativePath);
1490
+
1491
+ const ciWorkflowRelativePath = '.github/workflows/ci.yml';
1492
+ const ciWorkflowTemplatePath = path.join(templateDir, ciWorkflowRelativePath);
1493
+ const ciWorkflowTargetPath = path.join(targetDir, ciWorkflowRelativePath);
1494
+
1495
+ const variables = {
1496
+ PACKAGE_NAME: options.packageName,
1497
+ DEFAULT_BRANCH: options.defaultBranch,
1498
+ BETA_BRANCH: options.betaBranch,
1499
+ SCOPE: options.scope
1500
+ };
1501
+
1502
+ reporter.start('workflow-release', `Ensuring ${workflowRelativePath} includes stable+beta triggers...`);
1503
+ const workflowUpsert = upsertReleaseWorkflow(workflowTargetPath, workflowTemplatePath, {
1504
+ force: options.force,
1505
+ dryRun: options.dryRun,
1506
+ variables
1507
+ });
1508
+
1509
+ if (workflowUpsert.result === 'created') {
1510
+ summary.createdFiles.push(workflowRelativePath);
1511
+ reporter.ok('workflow-release', `${workflowRelativePath} created.`);
1512
+ } else if (workflowUpsert.result === 'overwritten' || workflowUpsert.result === 'updated') {
1513
+ summary.overwrittenFiles.push(workflowRelativePath);
1514
+ reporter.ok('workflow-release', `${workflowRelativePath} updated.`);
1515
+ } else {
1516
+ summary.skippedFiles.push(workflowRelativePath);
1517
+ if (workflowUpsert.warning) {
1518
+ summary.warnings.push(workflowUpsert.warning);
1519
+ reporter.warn('workflow-release', workflowUpsert.warning);
1520
+ } else {
1521
+ reporter.warn('workflow-release', `${workflowRelativePath} already configured; kept as-is.`);
1522
+ }
1523
+ }
1524
+
1525
+ reporter.start('workflow-ci', `Ensuring ${ciWorkflowRelativePath} includes stable+beta triggers...`);
1526
+ const ciWorkflowUpsert = upsertCiWorkflow(ciWorkflowTargetPath, ciWorkflowTemplatePath, {
1527
+ force: options.force,
1528
+ dryRun: options.dryRun,
1529
+ variables
1530
+ });
1531
+
1532
+ if (ciWorkflowUpsert.result === 'created') {
1533
+ summary.createdFiles.push(ciWorkflowRelativePath);
1534
+ reporter.ok('workflow-ci', `${ciWorkflowRelativePath} created.`);
1535
+ } else if (ciWorkflowUpsert.result === 'overwritten' || ciWorkflowUpsert.result === 'updated') {
1536
+ summary.overwrittenFiles.push(ciWorkflowRelativePath);
1537
+ reporter.ok('workflow-ci', `${ciWorkflowRelativePath} updated.`);
1538
+ } else {
1539
+ summary.skippedFiles.push(ciWorkflowRelativePath);
1540
+ if (ciWorkflowUpsert.warning) {
1541
+ summary.warnings.push(ciWorkflowUpsert.warning);
1542
+ reporter.warn('workflow-ci', ciWorkflowUpsert.warning);
1543
+ } else {
1544
+ reporter.warn('workflow-ci', `${ciWorkflowRelativePath} already configured; kept as-is.`);
1545
+ }
1546
+ }
1547
+ }
1548
+
1549
+ function prevalidateInitExecution(args, selections, dependencies = {}, reporter = new StepReporter()) {
1159
1550
  const deps = {
1160
1551
  exec: dependencies.exec || execCommand
1161
1552
  };
1162
1553
 
1554
+ const packageRoot = path.resolve(__dirname, '..');
1555
+ const templateDir = path.join(packageRoot, 'template');
1163
1556
  const targetDir = path.resolve(args.dir);
1557
+ const packageJsonPath = path.join(targetDir, 'package.json');
1558
+ const result = {
1559
+ deps,
1560
+ targetDir,
1561
+ templateDir,
1562
+ packageJsonPath,
1563
+ repo: '',
1564
+ packageName: '',
1565
+ existsOnNpm: true,
1566
+ betaBranchExists: false,
1567
+ existingMainRuleset: null,
1568
+ existingBetaRuleset: null,
1569
+ mainRulesetPayload: null,
1570
+ betaRulesetPayload: createBetaRulesetPayload(args.betaBranch)
1571
+ };
1572
+
1573
+ reporter.start('validate-local', 'Validating local project and templates...');
1164
1574
  if (!fs.existsSync(targetDir)) {
1575
+ reporter.fail('validate-local', `Directory not found: ${targetDir}`);
1165
1576
  throw new Error(`Directory not found: ${targetDir}`);
1166
1577
  }
1167
1578
 
1168
- const packageJsonPath = path.join(targetDir, 'package.json');
1169
1579
  if (!fs.existsSync(packageJsonPath)) {
1580
+ reporter.fail('validate-local', `package.json not found in ${targetDir}`);
1170
1581
  throw new Error(`package.json not found in ${targetDir}`);
1171
1582
  }
1172
1583
 
1584
+ if (!fs.existsSync(templateDir)) {
1585
+ reporter.fail('validate-local', `Template not found in ${templateDir}`);
1586
+ throw new Error(`Template not found in ${templateDir}`);
1587
+ }
1588
+
1173
1589
  const packageJson = readJsonFile(packageJsonPath);
1174
- if (!packageJson.name) {
1175
- throw new Error(`package.json in ${targetDir} must define "name".`);
1590
+ result.packageName = packageJson.name || packageDirFromName(path.basename(targetDir));
1591
+ reporter.ok('validate-local', 'Local project validation complete.');
1592
+
1593
+ if (selections.withGithub) {
1594
+ reporter.start('validate-gh', 'Validating GitHub CLI and authentication...');
1595
+ ensureGhAvailable(deps);
1596
+ reporter.ok('validate-gh', 'GitHub CLI available and authenticated.');
1597
+
1598
+ reporter.start('resolve-repo', 'Resolving repository target...');
1599
+ result.repo = resolveRepo({ repo: args.repo }, deps);
1600
+ reporter.ok('resolve-repo', `Using repository ${result.repo}.`);
1601
+
1602
+ reporter.start('validate-main-branch', `Checking default branch "${args.defaultBranch}"...`);
1603
+ if (!branchExists(deps, result.repo, args.defaultBranch)) {
1604
+ reporter.fail('validate-main-branch', `Default branch "${args.defaultBranch}" was not found in ${result.repo}.`);
1605
+ throw new Error(`Default branch "${args.defaultBranch}" not found in ${result.repo}.`);
1606
+ }
1607
+ reporter.ok('validate-main-branch', `Default branch "${args.defaultBranch}" found.`);
1608
+
1609
+ reporter.start('validate-rulesets', 'Loading existing GitHub rulesets...');
1610
+ const rulesets = listRulesets(deps, result.repo);
1611
+ result.mainRulesetPayload = createRulesetPayload(args);
1612
+ result.existingMainRuleset = rulesets.find((item) => item.name === result.mainRulesetPayload.name) || null;
1613
+ if (selections.withBeta) {
1614
+ result.existingBetaRuleset = rulesets.find((item) => item.name === result.betaRulesetPayload.name) || null;
1615
+ }
1616
+ reporter.ok('validate-rulesets', 'Ruleset scan completed.');
1617
+
1618
+ if (selections.withBeta) {
1619
+ reporter.start('validate-beta-branch', `Checking beta branch "${args.betaBranch}"...`);
1620
+ result.betaBranchExists = branchExists(deps, result.repo, args.betaBranch);
1621
+ reporter.ok(
1622
+ 'validate-beta-branch',
1623
+ result.betaBranchExists
1624
+ ? `Beta branch "${args.betaBranch}" already exists.`
1625
+ : `Beta branch "${args.betaBranch}" will be created from "${args.defaultBranch}".`
1626
+ );
1627
+ }
1628
+ }
1629
+
1630
+ if (selections.withNpm) {
1631
+ reporter.start('validate-npm', 'Validating npm CLI and authentication...');
1632
+ ensureNpmAvailable(deps);
1633
+ ensureNpmAuthenticated(deps);
1634
+ reporter.ok('validate-npm', 'npm CLI available and authenticated.');
1635
+
1636
+ reporter.start('validate-package-publish', `Checking npm package status for ${result.packageName}...`);
1637
+ result.existsOnNpm = packageExistsOnNpm(deps, result.packageName);
1638
+ reporter.ok(
1639
+ 'validate-package-publish',
1640
+ result.existsOnNpm
1641
+ ? `Package ${result.packageName} already exists on npm.`
1642
+ : `Package ${result.packageName} does not exist on npm; first publish will run.`
1643
+ );
1644
+ }
1645
+
1646
+ return result;
1647
+ }
1648
+
1649
+ async function confirmInitPlan(args, selections, context, summary) {
1650
+ const hasExternalActions = selections.withGithub || selections.withNpm || selections.withBeta;
1651
+ const needsLocalForceConfirm = false;
1652
+
1653
+ if (!hasExternalActions && !needsLocalForceConfirm) {
1654
+ return;
1655
+ }
1656
+
1657
+ if (args.yes) {
1658
+ summary.warnings.push('Confirmation prompts skipped due to --yes.');
1659
+ return;
1176
1660
  }
1177
1661
 
1662
+ await confirmOrThrow(summarizePlannedInitActions(selections, args, context));
1663
+
1664
+ if (args.force) {
1665
+ await confirmOrThrow('--force will overwrite managed files/scripts/dependencies when applicable.');
1666
+ }
1667
+
1668
+ if (selections.withGithub && context.existingMainRuleset) {
1669
+ await confirmOrThrow(`Ruleset "${context.mainRulesetPayload.name}" already exists and will be overwritten.`);
1670
+ }
1671
+
1672
+ if (selections.withBeta && context.betaBranchExists) {
1673
+ await confirmOrThrow(`Branch "${args.betaBranch}" already exists and will be used as beta release flow branch.`);
1674
+ }
1675
+
1676
+ if (selections.withBeta && context.existingBetaRuleset) {
1677
+ await confirmOrThrow(`Ruleset "${context.betaRulesetPayload.name}" already exists and will be overwritten.`);
1678
+ }
1679
+ }
1680
+
1681
+ function runNpmSetup(args, dependencies = {}, options = {}) {
1682
+ const deps = {
1683
+ exec: dependencies.exec || execCommand
1684
+ };
1685
+ const reporter = options.reporter || new StepReporter();
1686
+ const summary = options.summary || createSummary();
1687
+
1688
+ const targetDir = path.resolve(args.dir);
1689
+ const packageJsonPath = path.join(targetDir, 'package.json');
1690
+ const packageJson = readJsonFile(packageJsonPath);
1691
+ const publishMissingByDefault = Boolean(options.publishMissingByDefault);
1692
+ const shouldPublishFirst = args.publishFirst || publishMissingByDefault;
1693
+
1694
+ reporter.start('npm-auth', 'Checking npm authentication...');
1178
1695
  ensureNpmAvailable(deps);
1179
1696
  ensureNpmAuthenticated(deps);
1697
+ reporter.ok('npm-auth', 'npm authentication validated.');
1180
1698
 
1181
- const summary = createSummary();
1182
1699
  summary.updatedScriptKeys.push('npm.auth', 'npm.package.lookup');
1183
1700
 
1184
1701
  if (!packageJson.publishConfig || packageJson.publishConfig.access !== 'public') {
1185
1702
  summary.warnings.push('package.json publishConfig.access is not "public". First publish may fail for public packages.');
1186
1703
  }
1187
1704
 
1705
+ reporter.start('npm-exists', `Checking whether ${packageJson.name} exists on npm...`);
1188
1706
  const existsOnNpm = packageExistsOnNpm(deps, packageJson.name);
1707
+ reporter.ok(
1708
+ 'npm-exists',
1709
+ existsOnNpm
1710
+ ? `Package ${packageJson.name} already exists on npm.`
1711
+ : `Package ${packageJson.name} is not published on npm yet.`
1712
+ );
1713
+
1189
1714
  if (existsOnNpm) {
1190
1715
  summary.skippedScriptKeys.push('npm.first_publish');
1716
+ summary.warnings.push(`Package "${packageJson.name}" already exists. First publish is not required.`);
1191
1717
  } else {
1192
1718
  summary.updatedScriptKeys.push('npm.first_publish_required');
1193
1719
  }
1194
1720
 
1195
- if (!existsOnNpm && !args.publishFirst) {
1721
+ if (!existsOnNpm && !shouldPublishFirst) {
1196
1722
  summary.warnings.push(`package "${packageJson.name}" was not found on npm. Run "create-package-starter setup-npm --dir ${targetDir} --publish-first" to perform first publish.`);
1197
1723
  }
1198
1724
 
1199
- if (args.publishFirst) {
1200
- if (existsOnNpm) {
1201
- summary.warnings.push(`package "${packageJson.name}" already exists on npm. Skipping first publish.`);
1202
- } else if (args.dryRun) {
1725
+ if (!existsOnNpm && shouldPublishFirst) {
1726
+ if (args.dryRun) {
1203
1727
  summary.warnings.push(`dry-run: would run "npm publish --access public" in ${targetDir}`);
1204
1728
  } else {
1729
+ reporter.start('npm-publish', `Publishing first version of ${packageJson.name}...`);
1205
1730
  const publish = deps.exec('npm', ['publish', '--access', 'public'], { cwd: targetDir, stdio: 'inherit' });
1206
1731
  if (publish.status !== 0) {
1732
+ reporter.fail('npm-publish', 'First publish failed.');
1207
1733
  const publishOutput = `${publish.stderr || ''}\n${publish.stdout || ''}`.toLowerCase();
1208
1734
  const isOtpError = publishOutput.includes('eotp') || publishOutput.includes('one-time password');
1209
1735
 
@@ -1220,6 +1746,8 @@ function setupNpm(args, dependencies = {}) {
1220
1746
 
1221
1747
  throw new Error('First publish failed. Check npm output above and try again.');
1222
1748
  }
1749
+
1750
+ reporter.ok('npm-publish', `First publish for ${packageJson.name} completed.`);
1223
1751
  summary.updatedScriptKeys.push('npm.first_publish_done');
1224
1752
  }
1225
1753
  }
@@ -1227,6 +1755,29 @@ function setupNpm(args, dependencies = {}) {
1227
1755
  summary.warnings.push('Configure npm Trusted Publisher manually in npm package settings after first publish.');
1228
1756
  summary.warnings.push('Trusted Publisher requires owner, repository, workflow file (.github/workflows/release.yml), and branch (main by default).');
1229
1757
 
1758
+ return summary;
1759
+ }
1760
+
1761
+ function setupNpm(args, dependencies = {}) {
1762
+ const targetDir = path.resolve(args.dir);
1763
+ if (!fs.existsSync(targetDir)) {
1764
+ throw new Error(`Directory not found: ${targetDir}`);
1765
+ }
1766
+
1767
+ const packageJsonPath = path.join(targetDir, 'package.json');
1768
+ if (!fs.existsSync(packageJsonPath)) {
1769
+ throw new Error(`package.json not found in ${targetDir}`);
1770
+ }
1771
+
1772
+ const packageJson = readJsonFile(packageJsonPath);
1773
+ if (!packageJson.name) {
1774
+ throw new Error(`package.json in ${targetDir} must define "name".`);
1775
+ }
1776
+
1777
+ const summary = runNpmSetup(args, dependencies, {
1778
+ reporter: new StepReporter(),
1779
+ publishMissingByDefault: false
1780
+ });
1230
1781
  printSummary(`npm setup completed for ${packageJson.name}`, summary);
1231
1782
  }
1232
1783
 
@@ -1370,7 +1921,7 @@ async function setupBeta(args, dependencies = {}) {
1370
1921
  `- set Actions workflow permissions to write`,
1371
1922
  `- ensure branch "${args.betaBranch}" exists${doesBranchExist ? ' (already exists)' : ' (will be created)'}`,
1372
1923
  `- apply branch protection ruleset "${betaRulesetPayload.name}"`,
1373
- '- require CI status checks "CI / check (18) (pull_request)" and "CI / check (20) (pull_request)" on beta branch',
1924
+ `- require CI status check "${REQUIRED_CHECK_CONTEXT}" on beta branch`,
1374
1925
  `- update local ${workflowRelativePath} and package.json beta scripts`
1375
1926
  ].join('\n')
1376
1927
  );
@@ -1541,16 +2092,12 @@ function promoteStable(args, dependencies = {}) {
1541
2092
  printSummary(`stable promotion prepared for ${targetDir}`, summary);
1542
2093
  }
1543
2094
 
1544
- function setupGithub(args, dependencies = {}) {
2095
+ function applyGithubMainSetup(args, dependencies, summary, reporter) {
1545
2096
  const deps = {
1546
2097
  exec: dependencies.exec || execCommand
1547
2098
  };
1548
-
1549
- ensureGhAvailable(deps);
1550
-
1551
2099
  const repo = resolveRepo(args, deps);
1552
2100
  const rulesetPayload = createRulesetPayload(args);
1553
- const summary = createSummary();
1554
2101
 
1555
2102
  summary.updatedScriptKeys.push(
1556
2103
  'repository.default_branch',
@@ -1564,10 +2111,10 @@ function setupGithub(args, dependencies = {}) {
1564
2111
  summary.warnings.push(`dry-run: would update repository settings for ${repo}`);
1565
2112
  summary.warnings.push(`dry-run: would set actions workflow permissions to write for ${repo}`);
1566
2113
  summary.warnings.push(`dry-run: would upsert ruleset "${rulesetPayload.name}" for refs/heads/${args.defaultBranch}`);
1567
- printSummary(`GitHub settings dry-run for ${repo}`, summary);
1568
- return;
2114
+ return { repo, rulesetPayload };
1569
2115
  }
1570
2116
 
2117
+ reporter.start('github-main-settings', 'Applying GitHub repository settings...');
1571
2118
  const repoPayload = {
1572
2119
  default_branch: args.defaultBranch,
1573
2120
  delete_branch_on_merge: true,
@@ -1579,15 +2126,62 @@ function setupGithub(args, dependencies = {}) {
1579
2126
 
1580
2127
  const patchRepo = ghApi(deps, 'PATCH', `/repos/${repo}`, repoPayload);
1581
2128
  if (patchRepo.status !== 0) {
2129
+ reporter.fail('github-main-settings', 'Failed to update repository settings.');
1582
2130
  throw new Error(`Failed to update repository settings: ${patchRepo.stderr || patchRepo.stdout}`.trim());
1583
2131
  }
2132
+ reporter.ok('github-main-settings', 'Repository settings updated.');
1584
2133
 
2134
+ reporter.start('github-workflow-permissions', 'Applying GitHub Actions workflow permissions...');
1585
2135
  updateWorkflowPermissions(deps, repo);
2136
+ reporter.ok('github-workflow-permissions', 'Workflow permissions configured.');
1586
2137
 
2138
+ reporter.start('github-main-ruleset', `Applying ruleset "${rulesetPayload.name}"...`);
1587
2139
  const upsertResult = upsertRuleset(deps, repo, rulesetPayload);
2140
+ reporter.ok('github-main-ruleset', `Ruleset ${upsertResult}.`);
1588
2141
  summary.overwrittenFiles.push(`github-ruleset:${upsertResult}`);
2142
+ return { repo, rulesetPayload };
2143
+ }
2144
+
2145
+ function applyGithubBetaSetup(args, dependencies, summary, reporter, repo) {
2146
+ const deps = {
2147
+ exec: dependencies.exec || execCommand
2148
+ };
2149
+ const betaRulesetPayload = createBetaRulesetPayload(args.betaBranch);
2150
+
2151
+ summary.updatedScriptKeys.push('github.beta_branch', 'github.beta_ruleset');
2152
+
2153
+ if (args.dryRun) {
2154
+ summary.warnings.push(`dry-run: would ensure branch "${args.betaBranch}" exists in ${repo}`);
2155
+ summary.warnings.push(`dry-run: would upsert ruleset "${betaRulesetPayload.name}" for refs/heads/${args.betaBranch}`);
2156
+ return;
2157
+ }
2158
+
2159
+ reporter.start('github-beta-branch', `Ensuring branch "${args.betaBranch}" exists...`);
2160
+ const branchResult = ensureBranchExists(deps, repo, args.defaultBranch, args.betaBranch);
2161
+ if (branchResult === 'created') {
2162
+ summary.createdFiles.push(`github-branch:${args.betaBranch}`);
2163
+ reporter.ok('github-beta-branch', `Branch "${args.betaBranch}" created.`);
2164
+ } else {
2165
+ summary.skippedFiles.push(`github-branch:${args.betaBranch}`);
2166
+ reporter.warn('github-beta-branch', `Branch "${args.betaBranch}" already exists.`);
2167
+ }
2168
+
2169
+ reporter.start('github-beta-ruleset', `Applying beta ruleset "${betaRulesetPayload.name}"...`);
2170
+ const upsertResult = upsertRuleset(deps, repo, betaRulesetPayload);
2171
+ summary.overwrittenFiles.push(`github-beta-ruleset:${upsertResult}`);
2172
+ reporter.ok('github-beta-ruleset', `Beta ruleset ${upsertResult}.`);
2173
+ }
2174
+
2175
+ function setupGithub(args, dependencies = {}) {
2176
+ const summary = createSummary();
2177
+ const deps = {
2178
+ exec: dependencies.exec || execCommand
2179
+ };
2180
+ ensureGhAvailable(deps);
1589
2181
 
1590
- printSummary(`GitHub settings applied to ${repo}`, summary);
2182
+ const reporter = new StepReporter();
2183
+ const { repo } = applyGithubMainSetup(args, dependencies, summary, reporter);
2184
+ printSummary(args.dryRun ? `GitHub settings dry-run for ${repo}` : `GitHub settings applied to ${repo}`, summary);
1591
2185
  }
1592
2186
 
1593
2187
  async function run(argv, dependencies = {}) {
@@ -1599,7 +2193,7 @@ async function run(argv, dependencies = {}) {
1599
2193
  }
1600
2194
 
1601
2195
  if (parsed.mode === 'init') {
1602
- initExistingPackage(parsed.args);
2196
+ await initExistingPackage(parsed.args, dependencies);
1603
2197
  return;
1604
2198
  }
1605
2199
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i-santos/create-package-starter",
3
- "version": "1.5.0-beta.2",
3
+ "version": "1.5.0-beta.4",
4
4
  "description": "Scaffold new npm packages with a standardized Changesets release workflow",
5
5
  "license": "MIT",
6
6
  "author": "Igor Santos",
@@ -28,3 +28,17 @@ jobs:
28
28
 
29
29
  - name: Check
30
30
  run: npm run check
31
+
32
+ required-check:
33
+ name: required-check
34
+ runs-on: ubuntu-latest
35
+ needs:
36
+ - check
37
+ if: ${{ always() }}
38
+ steps:
39
+ - name: Validate matrix result
40
+ run: |
41
+ if [ "${{ needs.check.result }}" != "success" ]; then
42
+ echo "check matrix failed"
43
+ exit 1
44
+ fi
@@ -27,6 +27,9 @@ jobs:
27
27
  cache: npm
28
28
  registry-url: https://registry.npmjs.org
29
29
 
30
+ - name: Setup npm (latest)
31
+ run: npm install -g npm@latest
32
+
30
33
  - name: Install
31
34
  run: npm ci
32
35
 
@@ -42,3 +45,4 @@ jobs:
42
45
  commit: "chore: release packages"
43
46
  env:
44
47
  GITHUB_TOKEN: ${{ secrets.CHANGESETS_GH_TOKEN || secrets.GITHUB_TOKEN }}
48
+ NODE_AUTH_TOKEN: ""
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ .npmrc