@i-santos/create-package-starter 1.2.0 → 1.4.0

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.
Files changed (3) hide show
  1. package/README.md +22 -1
  2. package/lib/run.js +174 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,6 +9,7 @@ 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
11
  npx @i-santos/create-package-starter setup-github --repo i-santos/firestack --dry-run
12
+ npx @i-santos/create-package-starter setup-npm --dir ./existing-package --publish-first
12
13
  ```
13
14
 
14
15
  ## Commands
@@ -36,6 +37,13 @@ Configure GitHub repository settings:
36
37
  - `--ruleset <path>` (optional JSON override)
37
38
  - `--dry-run` (prints intended operations only)
38
39
 
40
+ Bootstrap npm publishing:
41
+
42
+ - `setup-npm`
43
+ - `--dir <directory>` (default: current directory)
44
+ - `--publish-first` (run `npm publish --access public` only when package is not found on npm)
45
+ - `--dry-run` (prints intended operations only)
46
+
39
47
  ## Managed Standards
40
48
 
41
49
  The generated and managed baseline includes:
@@ -79,10 +87,23 @@ All commands print a deterministic summary with:
79
87
  - delete branch on merge
80
88
  - auto-merge enabled
81
89
  - squash-only merge policy
82
- - create/update branch ruleset with required PR, 1 approval, stale review dismissal, resolved conversations, and deletion/force-push protection
90
+ - set Actions workflow default permissions to `write` (with PR review approvals enabled for workflows)
91
+ - create/update branch ruleset with required PR, 0 approvals by default, stale review dismissal, resolved conversations, and deletion/force-push protection
83
92
 
84
93
  If `gh` is missing or unauthenticated, command exits non-zero with actionable guidance.
85
94
 
95
+ ## setup-npm Behavior
96
+
97
+ `setup-npm` validates npm publish readiness:
98
+
99
+ - checks npm CLI availability
100
+ - checks npm authentication (`npm whoami`)
101
+ - checks whether package already exists on npm
102
+ - optionally performs first publish (`--publish-first`)
103
+ - prints next steps for Trusted Publisher configuration
104
+
105
+ Important: Trusted Publisher still needs manual setup in npm package settings.
106
+
86
107
  ## Trusted Publishing Note
87
108
 
88
109
  If package does not exist on npm yet, first publish may be manual:
package/lib/run.js CHANGED
@@ -25,13 +25,15 @@ function usage() {
25
25
  ' create-package-starter --name <name> [--out <directory>] [--default-branch <branch>]',
26
26
  ' create-package-starter init [--dir <directory>] [--force] [--cleanup-legacy-release] [--scope <scope>] [--default-branch <branch>]',
27
27
  ' create-package-starter setup-github [--repo <owner/repo>] [--default-branch <branch>] [--ruleset <path>] [--dry-run]',
28
+ ' create-package-starter setup-npm [--dir <directory>] [--publish-first] [--dry-run]',
28
29
  '',
29
30
  'Examples:',
30
31
  ' create-package-starter --name hello-package',
31
32
  ' create-package-starter --name @i-santos/swarm --out ./packages',
32
33
  ' create-package-starter init --dir ./my-package',
33
34
  ' create-package-starter init --cleanup-legacy-release',
34
- ' create-package-starter setup-github --repo i-santos/firestack --dry-run'
35
+ ' create-package-starter setup-github --repo i-santos/firestack --dry-run',
36
+ ' create-package-starter setup-npm --dir . --publish-first'
35
37
  ].join('\n');
36
38
  }
37
39
 
@@ -176,6 +178,43 @@ function parseSetupGithubArgs(argv) {
176
178
  return args;
177
179
  }
178
180
 
181
+ function parseSetupNpmArgs(argv) {
182
+ const args = {
183
+ dir: process.cwd(),
184
+ publishFirst: false,
185
+ dryRun: false
186
+ };
187
+
188
+ for (let i = 0; i < argv.length; i += 1) {
189
+ const token = argv[i];
190
+
191
+ if (token === '--dir') {
192
+ args.dir = parseValueFlag(argv, i, '--dir');
193
+ i += 1;
194
+ continue;
195
+ }
196
+
197
+ if (token === '--publish-first') {
198
+ args.publishFirst = true;
199
+ continue;
200
+ }
201
+
202
+ if (token === '--dry-run') {
203
+ args.dryRun = true;
204
+ continue;
205
+ }
206
+
207
+ if (token === '--help' || token === '-h') {
208
+ args.help = true;
209
+ continue;
210
+ }
211
+
212
+ throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
213
+ }
214
+
215
+ return args;
216
+ }
217
+
179
218
  function parseArgs(argv) {
180
219
  if (argv[0] === 'init') {
181
220
  return {
@@ -191,6 +230,13 @@ function parseArgs(argv) {
191
230
  };
192
231
  }
193
232
 
233
+ if (argv[0] === 'setup-npm') {
234
+ return {
235
+ mode: 'setup-npm',
236
+ args: parseSetupNpmArgs(argv.slice(1))
237
+ };
238
+ }
239
+
194
240
  return {
195
241
  mode: 'create',
196
242
  args: parseCreateArgs(argv)
@@ -581,7 +627,7 @@ function createBaseRulesetPayload(defaultBranch) {
581
627
  {
582
628
  type: 'pull_request',
583
629
  parameters: {
584
- required_approving_review_count: 1,
630
+ required_approving_review_count: 0,
585
631
  dismiss_stale_reviews_on_push: true,
586
632
  require_code_owner_review: false,
587
633
  require_last_push_approval: false,
@@ -663,6 +709,115 @@ function upsertRuleset(deps, repo, rulesetPayload) {
663
709
  return 'updated';
664
710
  }
665
711
 
712
+ function updateWorkflowPermissions(deps, repo) {
713
+ const workflowPermissionsPayload = {
714
+ default_workflow_permissions: 'write',
715
+ can_approve_pull_request_reviews: true
716
+ };
717
+
718
+ const result = ghApi(
719
+ deps,
720
+ 'PUT',
721
+ `/repos/${repo}/actions/permissions/workflow`,
722
+ workflowPermissionsPayload
723
+ );
724
+
725
+ if (result.status !== 0) {
726
+ throw new Error(
727
+ `Failed to update workflow permissions: ${result.stderr || result.stdout}`.trim()
728
+ );
729
+ }
730
+ }
731
+
732
+ function ensureNpmAvailable(deps) {
733
+ const version = deps.exec('npm', ['--version']);
734
+ if (version.status !== 0) {
735
+ throw new Error('npm CLI is required. Install npm and rerun.');
736
+ }
737
+ }
738
+
739
+ function ensureNpmAuthenticated(deps) {
740
+ const whoami = deps.exec('npm', ['whoami']);
741
+ if (whoami.status !== 0) {
742
+ throw new Error('npm CLI is not authenticated. Run "npm login" and rerun.');
743
+ }
744
+ }
745
+
746
+ function packageExistsOnNpm(deps, packageName) {
747
+ const view = deps.exec('npm', ['view', packageName, 'version', '--json']);
748
+ if (view.status === 0) {
749
+ return true;
750
+ }
751
+
752
+ const output = `${view.stderr || ''}\n${view.stdout || ''}`.toLowerCase();
753
+ if (output.includes('e404') || output.includes('not found') || output.includes('404')) {
754
+ return false;
755
+ }
756
+
757
+ throw new Error(`Failed to check package on npm: ${view.stderr || view.stdout}`.trim());
758
+ }
759
+
760
+ function setupNpm(args, dependencies = {}) {
761
+ const deps = {
762
+ exec: dependencies.exec || execCommand
763
+ };
764
+
765
+ const targetDir = path.resolve(args.dir);
766
+ if (!fs.existsSync(targetDir)) {
767
+ throw new Error(`Directory not found: ${targetDir}`);
768
+ }
769
+
770
+ const packageJsonPath = path.join(targetDir, 'package.json');
771
+ if (!fs.existsSync(packageJsonPath)) {
772
+ throw new Error(`package.json not found in ${targetDir}`);
773
+ }
774
+
775
+ const packageJson = readJsonFile(packageJsonPath);
776
+ if (!packageJson.name) {
777
+ throw new Error(`package.json in ${targetDir} must define "name".`);
778
+ }
779
+
780
+ ensureNpmAvailable(deps);
781
+ ensureNpmAuthenticated(deps);
782
+
783
+ const summary = createSummary();
784
+ summary.updatedScriptKeys.push('npm.auth', 'npm.package.lookup');
785
+
786
+ if (!packageJson.publishConfig || packageJson.publishConfig.access !== 'public') {
787
+ summary.warnings.push('package.json publishConfig.access is not "public". First publish may fail for public packages.');
788
+ }
789
+
790
+ const existsOnNpm = packageExistsOnNpm(deps, packageJson.name);
791
+ if (existsOnNpm) {
792
+ summary.skippedScriptKeys.push('npm.first_publish');
793
+ } else {
794
+ summary.updatedScriptKeys.push('npm.first_publish_required');
795
+ }
796
+
797
+ if (!existsOnNpm && !args.publishFirst) {
798
+ 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.`);
799
+ }
800
+
801
+ if (args.publishFirst) {
802
+ if (existsOnNpm) {
803
+ summary.warnings.push(`package "${packageJson.name}" already exists on npm. Skipping first publish.`);
804
+ } else if (args.dryRun) {
805
+ summary.warnings.push(`dry-run: would run "npm publish --access public" in ${targetDir}`);
806
+ } else {
807
+ const publish = deps.exec('npm', ['publish', '--access', 'public'], { cwd: targetDir });
808
+ if (publish.status !== 0) {
809
+ throw new Error(`First publish failed: ${(publish.stderr || publish.stdout || '').trim()}`);
810
+ }
811
+ summary.updatedScriptKeys.push('npm.first_publish_done');
812
+ }
813
+ }
814
+
815
+ summary.warnings.push('Configure npm Trusted Publisher manually in npm package settings after first publish.');
816
+ summary.warnings.push('Trusted Publisher requires owner, repository, workflow file (.github/workflows/release.yml), and branch (main by default).');
817
+
818
+ printSummary(`npm setup completed for ${packageJson.name}`, summary);
819
+ }
820
+
666
821
  function setupGithub(args, dependencies = {}) {
667
822
  const deps = {
668
823
  exec: dependencies.exec || execCommand
@@ -674,10 +829,17 @@ function setupGithub(args, dependencies = {}) {
674
829
  const rulesetPayload = createRulesetPayload(args);
675
830
  const summary = createSummary();
676
831
 
677
- summary.updatedScriptKeys.push('repository.default_branch', 'repository.delete_branch_on_merge', 'repository.allow_auto_merge', 'repository.merge_policy');
832
+ summary.updatedScriptKeys.push(
833
+ 'repository.default_branch',
834
+ 'repository.delete_branch_on_merge',
835
+ 'repository.allow_auto_merge',
836
+ 'repository.merge_policy',
837
+ 'actions.default_workflow_permissions'
838
+ );
678
839
 
679
840
  if (args.dryRun) {
680
841
  summary.warnings.push(`dry-run: would update repository settings for ${repo}`);
842
+ summary.warnings.push(`dry-run: would set actions workflow permissions to write for ${repo}`);
681
843
  summary.warnings.push(`dry-run: would upsert ruleset "${rulesetPayload.name}" for refs/heads/${args.defaultBranch}`);
682
844
  printSummary(`GitHub settings dry-run for ${repo}`, summary);
683
845
  return;
@@ -697,6 +859,8 @@ function setupGithub(args, dependencies = {}) {
697
859
  throw new Error(`Failed to update repository settings: ${patchRepo.stderr || patchRepo.stdout}`.trim());
698
860
  }
699
861
 
862
+ updateWorkflowPermissions(deps, repo);
863
+
700
864
  const upsertResult = upsertRuleset(deps, repo, rulesetPayload);
701
865
  summary.overwrittenFiles.push(`github-ruleset:${upsertResult}`);
702
866
 
@@ -721,6 +885,11 @@ async function run(argv, dependencies = {}) {
721
885
  return;
722
886
  }
723
887
 
888
+ if (parsed.mode === 'setup-npm') {
889
+ setupNpm(parsed.args, dependencies);
890
+ return;
891
+ }
892
+
724
893
  createNewPackage(parsed.args);
725
894
  }
726
895
 
@@ -728,5 +897,6 @@ module.exports = {
728
897
  run,
729
898
  parseRepoFromRemote,
730
899
  createBaseRulesetPayload,
731
- setupGithub
900
+ setupGithub,
901
+ setupNpm
732
902
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i-santos/create-package-starter",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Scaffold new npm packages with a standardized Changesets release workflow",
5
5
  "license": "MIT",
6
6
  "author": "Igor Santos",