create-three-blocks-starter 0.0.7 → 0.0.9

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 +39 -2
  2. package/bin/index.js +395 -210
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,9 +7,15 @@ Private CLI to scaffold a new project from the Three Blocks starter.
7
7
  Authenticate to the private registry first (one time per project):
8
8
 
9
9
  ```bash
10
- npx -y three-blocks-login --mode project --scope @three-blocks
10
+ # Using pnpm (recommended - no warnings)
11
+ pnpm dlx three-blocks-login@latest@latest --mode project --scope @three-blocks --channel stable
12
+
13
+ # Using npx (may show harmless npm config warnings)
14
+ npx -y three-blocks-login@latest --mode project --scope @three-blocks --channel stable
11
15
  ```
12
16
 
17
+ > **Note:** If you see npm warnings about unknown env configs, use `pnpm dlx` instead of `npx`, or see [three-blocks-login NPM warnings fix](https://www.npmjs.com/package/three-blocks-login).
18
+
13
19
  Then scaffold a new app:
14
20
 
15
21
  ```bash
@@ -26,6 +32,37 @@ pnpm i
26
32
  pnpm dev
27
33
  ```
28
34
 
35
+ ## CI/CD & Vercel Setup
36
+
37
+ The CLI now automatically generates the correct setup, but if you need to configure it manually:
38
+
39
+ ### Quick Setup (3 steps)
40
+
41
+ 1. **Commit `.npmrc`** to your repository (generated automatically by create-three-blocks-starter):
42
+ ```
43
+ @three-blocks:registry=https://three-blocks-196905988268.d.codeartifact.ap-northeast-1.amazonaws.com/npm/core/
44
+ ```
45
+
46
+ 2. **Preinstall script** (added automatically by create-three-blocks-starter):
47
+ ```json
48
+ {
49
+ "scripts": {
50
+ "preinstall": "npx -y three-blocks-login@latest"
51
+ }
52
+ }
53
+ ```
54
+
55
+ 3. **Set environment variable** in your CI/CD platform:
56
+ - **Vercel**: Project Settings → Environment Variables → Add `THREE_BLOCKS_SECRET_KEY`
57
+ - **GitHub Actions**: Repository Settings → Secrets → Add `THREE_BLOCKS_SECRET_KEY`
58
+ - **Other CI**: Add `THREE_BLOCKS_SECRET_KEY` secret with your license key
59
+
60
+ ### How It Works
61
+
62
+ - Committed `.npmrc` tells pnpm WHERE to find packages (no auth needed yet)
63
+ - Preinstall script fetches a short-lived token and adds it to `.npmrc`
64
+ - This solves pnpm's timing issue (it resolves packages before running preinstall)
65
+
29
66
  ## Development (monorepo)
30
67
 
31
68
  - The CLI pulls the template from `packages/three-blocks-starter/` during `prepack`.
@@ -38,6 +75,6 @@ pnpm --filter create-three-blocks-starter run dev:sync
38
75
  ## Publish
39
76
 
40
77
  ```bash
41
- npx -y three-blocks-login --mode project --scope @three-blocks
78
+ pnpm dlx three-blocks-login@latest --mode project --scope @three-blocks
42
79
  pnpm --filter create-three-blocks-starter publish --access restricted
43
80
  ```
package/bin/index.js CHANGED
@@ -20,13 +20,17 @@ const LOGIN_CLI = 'three-blocks-login'; // public login CLI
20
20
  const LOGIN_SPEC = `${LOGIN_CLI}@latest`; // force-fresh npx install
21
21
  const SCOPE = '@three-blocks';
22
22
 
23
- const BLOCKED_NPM_ENV_KEYS = new Set( [
23
+ const RAW_BLOCKED_NPM_ENV_KEYS = [
24
24
  'npm_config__three_blocks_registry',
25
25
  'npm_config_verify_deps_before_run',
26
26
  'npm_config_global_bin_dir',
27
27
  'npm_config__jsr_registry',
28
28
  'npm_config_node_linker',
29
- ].map( ( key ) => key.replace( /-/g, '_' ).toLowerCase() ) );
29
+ ];
30
+ const BLOCKED_NPM_ENV_KEYS = new Set( RAW_BLOCKED_NPM_ENV_KEYS.map( ( key ) => key.replace( /-/g, '_' ).toLowerCase() ) );
31
+ const LOGIN_HELPER_RELATIVE_PATH = path.join( 'scripts', 'three-blocks-login.cjs' );
32
+ const LOGIN_HELPER_POSIX_PATH = LOGIN_HELPER_RELATIVE_PATH.split( path.sep ).join( '/' );
33
+ const LOGIN_HELPER_COMMAND = `node ./${LOGIN_HELPER_POSIX_PATH}`;
30
34
 
31
35
  let DEBUG = /^1|true|yes$/i.test( String( process.env.THREE_BLOCKS_DEBUG || '' ).trim() );
32
36
 
@@ -36,6 +40,24 @@ const logDebug = ( msg ) => {
36
40
 
37
41
  };
38
42
 
43
+ let tempDirForCleanup = '';
44
+ const cleanupTmpDir = () => {
45
+
46
+ if ( ! tempDirForCleanup ) return;
47
+ try {
48
+
49
+ fs.rmSync( tempDirForCleanup, { recursive: true, force: true } );
50
+
51
+ } catch ( cleanupErr ) {
52
+
53
+ logDebug( `Unable to remove temp directory ${tempDirForCleanup}: ${cleanupErr?.message || cleanupErr}` );
54
+
55
+ }
56
+
57
+ tempDirForCleanup = '';
58
+
59
+ };
60
+
39
61
  class CliError extends Error {
40
62
 
41
63
  constructor( message, {
@@ -90,6 +112,7 @@ const cleanNpmEnv = ( extra = {} ) => {
90
112
 
91
113
  };
92
114
 
115
+
93
116
  const STEP_ICON = '⏺ ';
94
117
  const logInfo = ( msg ) => {
95
118
 
@@ -121,6 +144,111 @@ const logError = ( msg ) => {
121
144
 
122
145
  };
123
146
 
147
+ const appendAuthTokenLines = ( npmrcPath, authLines = [] ) => {
148
+
149
+ if ( ! authLines.length ) return false;
150
+ try {
151
+
152
+ const existing = fs.existsSync( npmrcPath ) ? fs.readFileSync( npmrcPath, 'utf8' ) : '';
153
+ const needsNewline = existing && ! existing.endsWith( os.EOL ) ? os.EOL : '';
154
+ const next = `${existing}${needsNewline}${authLines.join( os.EOL )}${os.EOL}`;
155
+ fs.writeFileSync( npmrcPath, next, { mode: 0o600 } );
156
+ return true;
157
+
158
+ } catch ( err ) {
159
+
160
+ logWarn( `Warning: could not append auth token to .npmrc: ${err?.message || err}` );
161
+ return false;
162
+
163
+ }
164
+
165
+ };
166
+
167
+ const stripAuthTokens = ( npmrcPath, hostPatterns = [] ) => {
168
+
169
+ if ( ! hostPatterns.length ) return;
170
+ try {
171
+
172
+ if ( ! fs.existsSync( npmrcPath ) ) return;
173
+ const raw = fs.readFileSync( npmrcPath, 'utf8' );
174
+ const lines = raw.split( /\r?\n/ );
175
+ const patterns = hostPatterns.map( ( host ) => new RegExp( `^\\s*\\/\\/${host.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' )}:_authToken\\s*=`, 'i' ) );
176
+ const filtered = lines.filter( ( line ) => ! patterns.some( ( re ) => re.test( line ) ) );
177
+ let end = filtered.length;
178
+ while ( end > 0 && ! filtered[ end - 1 ].trim() ) end --;
179
+ const compact = filtered.slice( 0, end ).join( os.EOL );
180
+ const finalContent = compact ? `${compact}${os.EOL}` : '';
181
+ fs.writeFileSync( npmrcPath, finalContent, { mode: 0o600 } );
182
+
183
+ } catch ( err ) {
184
+
185
+ logWarn( `Warning: could not scrub auth token from .npmrc: ${err?.message || err}` );
186
+
187
+ }
188
+
189
+ };
190
+
191
+ const buildLoginHelperContent = () => `#!/usr/bin/env node
192
+ 'use strict';
193
+
194
+ const { spawnSync } = require('node:child_process');
195
+
196
+ const RAW_BLOCKED_KEYS = ${JSON.stringify( RAW_BLOCKED_NPM_ENV_KEYS, null, 2 )};
197
+ const BLOCKED_KEYS = new Set(
198
+ RAW_BLOCKED_KEYS.map( ( key ) => key.replace( /-/g, '_' ).toLowerCase() )
199
+ );
200
+
201
+ const normalize = ( key ) => key.replace( /-/g, '_' ).toLowerCase();
202
+
203
+ const scrubEnv = ( source ) => {
204
+
205
+ const next = { ...source };
206
+ for ( const key of Object.keys( next ) ) {
207
+
208
+ if ( BLOCKED_KEYS.has( normalize( key ) ) ) {
209
+
210
+ delete next[ key ];
211
+
212
+ }
213
+
214
+ }
215
+
216
+ return next;
217
+
218
+ };
219
+
220
+ const shouldSkip = /^1|true|yes$/i.test( String( process.env.THREE_BLOCKS_LOGIN_SKIP || '' ) );
221
+ if ( shouldSkip ) {
222
+
223
+ process.exit( 0 );
224
+
225
+ }
226
+
227
+ const env = scrubEnv( process.env );
228
+ const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
229
+ const args = [ '-y', 'three-blocks-login@latest', ...process.argv.slice( 2 ) ];
230
+ const result = spawnSync( cmd, args, { stdio: 'inherit', env } );
231
+
232
+ if ( result.error ) {
233
+
234
+ console.error( result.error.message || result.error );
235
+ process.exit( 1 );
236
+
237
+ }
238
+
239
+ process.exit( result.status ?? 0 );
240
+ `;
241
+
242
+ const ensureLoginHelperScript = async ( targetDir ) => {
243
+
244
+ const helperPath = path.join( targetDir, LOGIN_HELPER_RELATIVE_PATH );
245
+ const helperDir = path.dirname( helperPath );
246
+ await fsp.mkdir( helperDir, { recursive: true } );
247
+ await fsp.writeFile( helperPath, buildLoginHelperContent(), { mode: 0o755 } );
248
+ return helperPath;
249
+
250
+ };
251
+
124
252
  const die = ( m ) => {
125
253
 
126
254
  throw new CliError( m );
@@ -405,7 +533,7 @@ const padText = ( value, width, align = 'left' ) => {
405
533
  const makeHeaderRow = ( left, right = '', leftAlign = 'left', rightAlign = 'left' ) =>
406
534
  `│${padText( left, LEFT_WIDTH, leftAlign )}│${padText( right, RIGHT_WIDTH, rightAlign )}│`;
407
535
 
408
- const HEADER_COLOR = ESC( 33 );
536
+ const HEADER_COLOR = ESC( '38;2;21;69;245' ); // threejs-blue (#1545F5)
409
537
  const CONTENT_COLOR = ESC( 90 );
410
538
  const reapplyColor = ( value, color ) => {
411
539
 
@@ -665,204 +793,205 @@ async function main() {
665
793
  const headerLicenseId = tokenMetadata?.licenseId ? String( tokenMetadata.licenseId ) : '';
666
794
 
667
795
  // 2) Pre-login in a TEMP dir to generate a temp .npmrc (no always-auth)
668
- let tmp = '';
669
- let tmpNpmrc = '';
796
+ let tmp = mkTmpDir();
797
+ tempDirForCleanup = tmp;
798
+ let tmpNpmrc = path.join( tmp, '.npmrc' );
799
+ logProgress( `Fetching short-lived token (temp .npmrc) [channel: ${channel}] ...` );
670
800
  try {
671
801
 
672
- tmp = mkTmpDir();
673
- tmpNpmrc = path.join( tmp, '.npmrc' );
674
- logProgress( `Fetching short-lived token (temp .npmrc) [channel: ${channel}] ...` );
675
- try {
676
-
677
- run( 'npx', [ '-y', LOGIN_SPEC, '--mode', 'project', '--scope', SCOPE, '--channel', channel ], {
678
- cwd: tmp,
679
- env: { ...process.env, THREE_BLOCKS_SECRET_KEY: license, THREE_BLOCKS_CHANNEL: channel },
680
- } );
681
-
682
- } catch ( err ) {
683
-
684
- if ( err instanceof CliError ) {
802
+ run( 'npx', [ '-y', LOGIN_SPEC, '--mode', 'project', '--scope', SCOPE, '--channel', channel ], {
803
+ cwd: tmp,
804
+ env: { ...process.env, THREE_BLOCKS_SECRET_KEY: license, THREE_BLOCKS_CHANNEL: channel },
805
+ } );
685
806
 
686
- throw new CliError(
687
- 'Login failed while minting short-lived token.',
688
- {
689
- exitCode: err.exitCode,
690
- command: err.command,
691
- args: err.args,
692
- suggestion: 'Ensure the three-blocks-login CLI is reachable and your license key is valid.',
693
- cause: err,
694
- }
695
- );
807
+ } catch ( err ) {
696
808
 
697
- }
809
+ if ( err instanceof CliError ) {
698
810
 
699
- throw err;
811
+ throw new CliError(
812
+ 'Login failed while minting short-lived token.',
813
+ {
814
+ exitCode: err.exitCode,
815
+ command: err.command,
816
+ args: err.args,
817
+ suggestion: 'Ensure the three-blocks-login CLI is reachable and your license key is valid.',
818
+ cause: err,
819
+ }
820
+ );
700
821
 
701
822
  }
702
823
 
703
- if ( ! fs.existsSync( tmpNpmrc ) ) {
704
-
705
- throw new CliError( 'Login failed: temp .npmrc not created.' );
706
-
707
- }
824
+ throw err;
708
825
 
709
- // Extract registry URL from temp .npmrc so we can pass it explicitly to npm create
710
- let registryUrl = '';
711
- try {
826
+ }
712
827
 
713
- const txt = fs.readFileSync( tmpNpmrc, 'utf8' );
714
- const m = txt.match( /^@[^:]+:registry=(.+)$/m );
715
- if ( m && m[ 1 ] ) registryUrl = m[ 1 ].trim();
828
+ if ( ! fs.existsSync( tmpNpmrc ) ) {
716
829
 
717
- } catch ( readErr ) {
830
+ throw new CliError( 'Login failed: temp .npmrc not created.' );
718
831
 
719
- logDebug( `Could not read temp .npmrc: ${readErr?.message || readErr}` );
832
+ }
720
833
 
721
- }
834
+ // Extract registry URL from temp .npmrc so we can pass it explicitly to npm create
835
+ let registryUrl = '';
836
+ const tempAuthLines = [];
837
+ const tempAuthHosts = new Set();
838
+ try {
722
839
 
723
- if ( ! headerRegistry && registryUrl ) headerRegistry = registryUrl;
840
+ const txt = fs.readFileSync( tmpNpmrc, 'utf8' );
841
+ const lines = txt.split( /\r?\n/ );
842
+ for ( const rawLine of lines ) {
724
843
 
725
- // 3) Scaffold the private starter by packing and extracting the tarball (avoids npm create naming transform)
726
- const starterSpec = `${STARTER_PKG}@${channel === 'stable' ? 'latest' : channel}`;
727
- logProgress( `Fetching starter tarball ${starterSpec} ...` );
728
- const createEnv = {
729
- ...process.env,
730
- THREE_BLOCKS_SECRET_KEY: license,
731
- NPM_CONFIG_USERCONFIG: tmpNpmrc,
732
- npm_config_userconfig: tmpNpmrc,
733
- };
734
- const packArgs = [ 'pack', starterSpec, '--json' ];
735
- if ( ! DEBUG ) packArgs.push( '--silent' );
736
- let packedOut = '';
737
- try {
844
+ const line = rawLine.replace( /\r/g, '' );
845
+ if ( ! line ) continue;
846
+ if ( ! registryUrl ) {
738
847
 
739
- packedOut = exec( 'npm', packArgs, { cwd: tmp, env: createEnv } );
848
+ const m = line.match( /^@[^:]+:registry=(.+)$/ );
849
+ if ( m && m[ 1 ] ) registryUrl = m[ 1 ].trim();
740
850
 
741
- } catch ( err ) {
851
+ }
742
852
 
743
- if ( err instanceof CliError ) {
853
+ const authMatch = line.match( /^\/\/([^:]+):_authToken=.+$/ );
854
+ if ( authMatch && authMatch[ 1 ] ) {
744
855
 
745
- const baseMessage = `Failed to fetch starter tarball ${starterSpec}.`;
746
- const detail = ( err.stderr || err.stdout || '' ).trim();
747
- const message = detail ? baseMessage : `${baseMessage} ${err.message || ''}`.trim();
748
- throw new CliError(
749
- message,
750
- {
751
- exitCode: err.exitCode,
752
- command: err.command,
753
- args: err.args,
754
- stdout: err.stdout,
755
- stderr: err.stderr,
756
- suggestion: 'Confirm your license has access to the selected channel and that the package version exists.',
757
- cause: err,
758
- }
759
- );
856
+ tempAuthLines.push( line );
857
+ tempAuthHosts.add( authMatch[ 1 ] );
760
858
 
761
859
  }
762
860
 
763
- throw new CliError(
764
- `Failed to fetch starter tarball ${starterSpec}.`,
765
- { cause: err }
766
- );
767
-
768
861
  }
769
862
 
770
- let tarName = '';
771
- let starterVersion = '';
772
- try {
773
-
774
- const info = JSON.parse( packedOut );
775
- if ( Array.isArray( info ) ) {
776
-
777
- tarName = info[ 0 ]?.filename || '';
778
- starterVersion = info[ 0 ]?.version || '';
863
+ } catch ( readErr ) {
779
864
 
780
- } else {
865
+ logDebug( `Could not read temp .npmrc: ${readErr?.message || readErr}` );
781
866
 
782
- tarName = info.filename || '';
783
- starterVersion = info.version || '';
784
-
785
- }
867
+ }
786
868
 
787
- } catch {
869
+ if ( ! headerRegistry && registryUrl ) headerRegistry = registryUrl;
870
+ const authHostList = Array.from( tempAuthHosts );
871
+
872
+ // 3) Scaffold the private starter by packing and extracting the tarball (avoids npm create naming transform)
873
+ const starterSpec = `${STARTER_PKG}@${channel === 'stable' ? 'latest' : channel}`;
874
+ logProgress( `Fetching starter tarball ${starterSpec} ...` );
875
+ const createEnv = {
876
+ ...process.env,
877
+ THREE_BLOCKS_SECRET_KEY: license,
878
+ NPM_CONFIG_USERCONFIG: tmpNpmrc,
879
+ npm_config_userconfig: tmpNpmrc,
880
+ };
881
+ const packArgs = [ 'pack', starterSpec, '--json' ];
882
+ if ( ! DEBUG ) packArgs.push( '--silent' );
883
+ let packedOut = '';
884
+ try {
788
885
 
789
- const lines = String( packedOut || '' ).trim().split( /\r?\n/ );
790
- tarName = lines[ lines.length - 1 ] || '';
886
+ packedOut = exec( 'npm', packArgs, { cwd: tmp, env: createEnv } );
791
887
 
792
- }
888
+ } catch ( err ) {
793
889
 
794
- const tarPath = path.join( tmp, tarName );
795
- if ( ! tarName || ! fs.existsSync( tarPath ) ) {
890
+ if ( err instanceof CliError ) {
796
891
 
892
+ const baseMessage = `Failed to fetch starter tarball ${starterSpec}.`;
893
+ const detail = ( err.stderr || err.stdout || '' ).trim();
894
+ const message = detail ? baseMessage : `${baseMessage} ${err.message || ''}`.trim();
797
895
  throw new CliError(
798
- `Failed to fetch starter tarball ${starterSpec}.`,
799
- { suggestion: 'Ensure the requested release is published and accessible for your channel.' }
896
+ message,
897
+ {
898
+ exitCode: err.exitCode,
899
+ command: err.command,
900
+ args: err.args,
901
+ stdout: err.stdout,
902
+ stderr: err.stderr,
903
+ suggestion: 'Confirm your license has access to the selected channel and that the package version exists.',
904
+ cause: err,
905
+ }
800
906
  );
801
907
 
802
908
  }
803
909
 
804
- const headerStarterVersion = starterVersion || extractVersionFromTarball( tarName );
805
- const headerCoreVersion = headerChannel === 'stable' ? 'latest' : headerChannel;
806
- renderHeader( {
807
- starterVersion: headerStarterVersion,
808
- userDisplayName,
809
- planName,
810
- repoPath,
811
- projectName: appName,
812
- channel: headerChannel,
813
- coreVersion: headerCoreVersion,
814
- license,
815
- registry: headerRegistry,
816
- domain: headerDomain,
817
- region: headerRegion,
818
- repository: headerRepository,
819
- expiresAt: headerExpires,
820
- teamId: headerTeamId,
821
- teamName: headerTeamName,
822
- licenseId: headerLicenseId,
823
- } );
824
- logProgress( 'Extracting files ...' );
825
- try {
910
+ throw new CliError(
911
+ `Failed to fetch starter tarball ${starterSpec}.`,
912
+ { cause: err }
913
+ );
826
914
 
827
- run( 'tar', [ '-xzf', tarPath, '-C', targetDir, '--strip-components=1' ] );
915
+ }
828
916
 
829
- } catch ( err ) {
917
+ let tarName = '';
918
+ let starterVersion = '';
919
+ try {
830
920
 
831
- if ( err instanceof CliError ) {
921
+ const info = JSON.parse( packedOut );
922
+ if ( Array.isArray( info ) ) {
832
923
 
833
- throw new CliError(
834
- 'Failed to extract starter files.',
835
- {
836
- exitCode: err.exitCode,
837
- command: err.command,
838
- args: err.args,
839
- suggestion: 'Check file permissions and available disk space before retrying.',
840
- cause: err,
841
- }
842
- );
924
+ tarName = info[ 0 ]?.filename || '';
925
+ starterVersion = info[ 0 ]?.version || '';
843
926
 
844
- }
927
+ } else {
845
928
 
846
- throw err;
929
+ tarName = info.filename || '';
930
+ starterVersion = info.version || '';
847
931
 
848
932
  }
849
933
 
850
- } finally {
934
+ } catch {
851
935
 
852
- if ( tmp ) {
936
+ const lines = String( packedOut || '' ).trim().split( /\r?\n/ );
937
+ tarName = lines[ lines.length - 1 ] || '';
853
938
 
854
- try {
939
+ }
855
940
 
856
- fs.rmSync( tmp, { recursive: true, force: true } );
941
+ const tarPath = path.join( tmp, tarName );
942
+ if ( ! tarName || ! fs.existsSync( tarPath ) ) {
857
943
 
858
- } catch ( cleanupErr ) {
944
+ throw new CliError(
945
+ `Failed to fetch starter tarball ${starterSpec}.`,
946
+ { suggestion: 'Ensure the requested release is published and accessible for your channel.' }
947
+ );
859
948
 
860
- logDebug( `Unable to remove temp directory ${tmp}: ${cleanupErr?.message || cleanupErr}` );
949
+ }
861
950
 
862
- }
951
+ const headerStarterVersion = starterVersion || extractVersionFromTarball( tarName );
952
+ const headerCoreVersion = headerChannel === 'stable' ? 'latest' : headerChannel;
953
+ renderHeader( {
954
+ starterVersion: headerStarterVersion,
955
+ userDisplayName,
956
+ planName,
957
+ repoPath,
958
+ projectName: appName,
959
+ channel: headerChannel,
960
+ coreVersion: headerCoreVersion,
961
+ license,
962
+ registry: headerRegistry,
963
+ domain: headerDomain,
964
+ region: headerRegion,
965
+ repository: headerRepository,
966
+ expiresAt: headerExpires,
967
+ teamId: headerTeamId,
968
+ teamName: headerTeamName,
969
+ licenseId: headerLicenseId,
970
+ } );
971
+ logProgress( 'Extracting files ...' );
972
+ try {
973
+
974
+ run( 'tar', [ '-xzf', tarPath, '-C', targetDir, '--strip-components=1' ] );
975
+
976
+ } catch ( err ) {
977
+
978
+ if ( err instanceof CliError ) {
979
+
980
+ throw new CliError(
981
+ 'Failed to extract starter files.',
982
+ {
983
+ exitCode: err.exitCode,
984
+ command: err.command,
985
+ args: err.args,
986
+ suggestion: 'Check file permissions and available disk space before retrying.',
987
+ cause: err,
988
+ }
989
+ );
863
990
 
864
991
  }
865
992
 
993
+ throw err;
994
+
866
995
  }
867
996
 
868
997
  // 4) Write .env.local and .gitignore entries
@@ -876,7 +1005,8 @@ async function main() {
876
1005
 
877
1006
  } catch {}
878
1007
 
879
- for ( const line of [ '.env.local', '.npmrc' ] ) {
1008
+ // Only add .env.local to .gitignore (NOT .npmrc - we want to commit the registry config)
1009
+ for ( const line of [ '.env.local' ] ) {
880
1010
 
881
1011
  if ( ! gi.includes( line ) ) gi += ( gi.endsWith( '\n' ) || gi === '' ? '' : '\n' ) + line + '\n';
882
1012
 
@@ -884,9 +1014,42 @@ async function main() {
884
1014
 
885
1015
  await fsp.writeFile( giPath, gi );
886
1016
 
887
- // 5) First install the starter already has:
888
- // "preinstall": "npx -y three-blocks-login --mode project --scope @three-blocks"
889
- // so it will mint a fresh token and write a project .npmrc.
1017
+ // Create base .npmrc with registry URL (tokens added dynamically)
1018
+ const npmrcPath = path.join( targetDir, '.npmrc' );
1019
+ const npmrcContent = [
1020
+ '# Three Blocks registry configuration',
1021
+ '# Auth tokens are appended automatically by three-blocks-login and expire quickly.',
1022
+ '# Avoid committing auth tokens; rerun pnpm install to refresh access if needed.',
1023
+ '',
1024
+ `${SCOPE}:registry=${headerRegistry || registryUrl}`,
1025
+ '',
1026
+ '# Auth tokens are appended during installs/preinstall hooks.',
1027
+ '# DO NOT hard-code long-lived tokens in this file.',
1028
+ ''
1029
+ ].join( '\n' );
1030
+ await fsp.writeFile( npmrcPath, npmrcContent ).catch( ( e ) => {
1031
+
1032
+ logWarn( `Warning: could not write .npmrc: ${e?.message || String( e )}` );
1033
+
1034
+ } );
1035
+ let loginHelperReady = false;
1036
+ try {
1037
+
1038
+ await ensureLoginHelperScript( targetDir );
1039
+ loginHelperReady = true;
1040
+ logSuccess( `Prepared login helper script (${LOGIN_HELPER_POSIX_PATH}).` );
1041
+
1042
+ } catch ( helperErr ) {
1043
+
1044
+ logWarn( `Warning: could not write ${LOGIN_HELPER_POSIX_PATH}: ${helperErr?.message || String( helperErr )}` );
1045
+
1046
+ }
1047
+
1048
+ const primedAuth = appendAuthTokenLines( npmrcPath, tempAuthLines );
1049
+ if ( primedAuth ) logDebug( 'Primed .npmrc with short-lived auth token.' );
1050
+
1051
+ // 5) Add simplified preinstall script
1052
+ // The script will automatically append auth token to existing .npmrc
890
1053
  try {
891
1054
 
892
1055
  const pkgPath = path.join( targetDir, 'package.json' );
@@ -895,12 +1058,12 @@ async function main() {
895
1058
  pkg.scripts = pkg.scripts || {};
896
1059
  if ( ! pkg.scripts.preinstall ) {
897
1060
 
898
- pkg.scripts.preinstall = `npx -y ${LOGIN_CLI}@latest --mode project --scope ${SCOPE} --channel ${channel}`;
1061
+ pkg.scripts.preinstall = loginHelperReady ? LOGIN_HELPER_COMMAND : `npx -y ${LOGIN_CLI}@latest`;
899
1062
 
900
1063
  }
901
1064
 
902
1065
  await fsp.writeFile( pkgPath, JSON.stringify( pkg, null, 2 ) );
903
- logSuccess( 'Added preinstall to generated package.json to refresh token on installs.' );
1066
+ logSuccess( 'Added preinstall script to refresh auth tokens on installs.' );
904
1067
 
905
1068
  } catch ( e ) {
906
1069
 
@@ -908,61 +1071,77 @@ async function main() {
908
1071
 
909
1072
  }
910
1073
 
911
- logProgress( 'Installing dependencies (preinstall will refresh token) ...' );
1074
+ const installMessage = primedAuth
1075
+ ? 'Installing dependencies (token primed; future installs refresh automatically) ...'
1076
+ : 'Installing dependencies (preinstall will refresh token) ...';
1077
+ logProgress( installMessage );
912
1078
  const hasPnpm = spawnSync( 'pnpm', [ '-v' ], { stdio: 'ignore', env: cleanNpmEnv() } ).status === 0;
1079
+ const sharedInstallEnv = {
1080
+ ...process.env,
1081
+ THREE_BLOCKS_SECRET_KEY: license,
1082
+ THREE_BLOCKS_CHANNEL: channel,
1083
+ };
1084
+ if ( primedAuth ) sharedInstallEnv.THREE_BLOCKS_LOGIN_SKIP = '1';
1085
+ const cleanupAuthAfterBootstrap = () => {
1086
+
1087
+ if ( primedAuth && authHostList.length ) stripAuthTokens( npmrcPath, authHostList );
1088
+
1089
+ };
1090
+
913
1091
  try {
914
1092
 
915
- run( hasPnpm ? 'pnpm' : 'npm', [ hasPnpm ? 'install' : 'ci' ], {
916
- cwd: targetDir,
917
- env: {
918
- ...process.env,
919
- THREE_BLOCKS_SECRET_KEY: license,
920
- THREE_BLOCKS_CHANNEL: channel,
921
- },
922
- } );
1093
+ try {
923
1094
 
924
- } catch ( err ) {
1095
+ run( hasPnpm ? 'pnpm' : 'npm', [ hasPnpm ? 'install' : 'ci' ], {
1096
+ cwd: targetDir,
1097
+ env: sharedInstallEnv,
1098
+ } );
925
1099
 
926
- if ( err instanceof CliError ) {
1100
+ } catch ( err ) {
927
1101
 
928
- throw new CliError(
929
- 'Dependency installation failed.',
930
- {
931
- exitCode: err.exitCode,
932
- command: err.command,
933
- args: err.args,
934
- stdout: err.stdout,
935
- stderr: err.stderr,
936
- suggestion: 'Inspect the log above for npm/pnpm errors or retry with --debug for full output.',
937
- cause: err,
938
- }
939
- );
1102
+ if ( err instanceof CliError ) {
1103
+
1104
+ throw new CliError(
1105
+ 'Dependency installation failed.',
1106
+ {
1107
+ exitCode: err.exitCode,
1108
+ command: err.command,
1109
+ args: err.args,
1110
+ stdout: err.stdout,
1111
+ stderr: err.stderr,
1112
+ suggestion: 'Inspect the log above for npm/pnpm errors or retry with --debug for full output.',
1113
+ cause: err,
1114
+ }
1115
+ );
1116
+
1117
+ }
1118
+
1119
+ throw err;
940
1120
 
941
1121
  }
942
1122
 
943
- throw err;
1123
+ {
944
1124
 
945
- }
1125
+ const coreSpec = `@three-blocks/core@${channel === 'stable' ? 'latest' : channel}`;
1126
+ logProgress( `Installing ${coreSpec} ...` );
1127
+ const addArgs = hasPnpm ? [ 'add', '--save-exact', coreSpec ] : [ 'install', '--save-exact', coreSpec ];
1128
+ const r = spawnSync( hasPnpm ? 'pnpm' : 'npm', addArgs, {
1129
+ stdio: 'inherit',
1130
+ cwd: targetDir,
1131
+ env: cleanNpmEnv( sharedInstallEnv ),
1132
+ } );
1133
+ if ( r.status !== 0 ) {
946
1134
 
947
- {
948
-
949
- const coreSpec = `@three-blocks/core@${channel === 'stable' ? 'latest' : channel}`;
950
- logProgress( `Installing ${coreSpec} ...` );
951
- const addArgs = hasPnpm ? [ 'add', '--save-exact', coreSpec ] : [ 'install', '--save-exact', coreSpec ];
952
- const r = spawnSync( hasPnpm ? 'pnpm' : 'npm', addArgs, {
953
- stdio: 'inherit',
954
- cwd: targetDir,
955
- env: cleanNpmEnv( {
956
- THREE_BLOCKS_SECRET_KEY: license,
957
- THREE_BLOCKS_CHANNEL: channel,
958
- } ),
959
- } );
960
- if ( r.status !== 0 ) {
1135
+ logWarn( `Warning: could not install @three-blocks/core (exit ${r.status}).` );
961
1136
 
962
- logWarn( `Warning: could not install @three-blocks/core (exit ${r.status}).` );
1137
+ }
963
1138
 
964
1139
  }
965
1140
 
1141
+ } finally {
1142
+
1143
+ cleanupAuthAfterBootstrap();
1144
+
966
1145
  }
967
1146
 
968
1147
  console.log( '' );
@@ -978,32 +1157,38 @@ async function main() {
978
1157
 
979
1158
  }
980
1159
 
981
- main().catch( ( err ) => {
1160
+ main()
1161
+ .catch( ( err ) => {
982
1162
 
983
- if ( err instanceof CliError ) {
1163
+ if ( err instanceof CliError ) {
984
1164
 
985
- logError( err.message );
986
- const detail = ( err.stderr || err.stdout || '' ).trim();
987
- if ( detail ) {
1165
+ logError( err.message );
1166
+ const detail = ( err.stderr || err.stdout || '' ).trim();
1167
+ if ( detail ) {
988
1168
 
989
- const lines = detail.split( /\r?\n/ );
990
- for ( const line of lines ) console.error( dim( ` ${line}` ) );
1169
+ const lines = detail.split( /\r?\n/ );
1170
+ for ( const line of lines ) console.error( dim( ` ${line}` ) );
991
1171
 
992
- }
1172
+ }
1173
+
1174
+ if ( err.suggestion ) logInfo( dim( `Hint: ${err.suggestion}` ) );
1175
+ if ( DEBUG && err.cause && err.cause !== err && err.cause?.stack ) {
993
1176
 
994
- if ( err.suggestion ) logInfo( dim( `Hint: ${err.suggestion}` ) );
995
- if ( DEBUG && err.cause && err.cause !== err && err.cause?.stack ) {
1177
+ console.error( dim( err.cause.stack ) );
1178
+
1179
+ }
996
1180
 
997
- console.error( dim( err.cause.stack ) );
1181
+ process.exit( err.exitCode ?? 1 );
998
1182
 
999
1183
  }
1000
1184
 
1001
- process.exit( err.exitCode ?? 1 );
1185
+ const fallback = err?.stack || err?.message || String( err );
1186
+ logError( fallback );
1187
+ process.exit( 1 );
1002
1188
 
1003
- }
1189
+ } )
1190
+ .finally( () => {
1004
1191
 
1005
- const fallback = err?.stack || err?.message || String( err );
1006
- logError( fallback );
1007
- process.exit( 1 );
1192
+ cleanupTmpDir();
1008
1193
 
1009
- } );
1194
+ } );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-three-blocks-starter",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Create a new Three Blocks starter app",