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.
- package/README.md +39 -2
- package/bin/index.js +395 -210
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
]
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
throw new CliError( 'Login failed: temp .npmrc not created.' );
|
|
706
|
-
|
|
707
|
-
}
|
|
824
|
+
throw err;
|
|
708
825
|
|
|
709
|
-
|
|
710
|
-
let registryUrl = '';
|
|
711
|
-
try {
|
|
826
|
+
}
|
|
712
827
|
|
|
713
|
-
|
|
714
|
-
const m = txt.match( /^@[^:]+:registry=(.+)$/m );
|
|
715
|
-
if ( m && m[ 1 ] ) registryUrl = m[ 1 ].trim();
|
|
828
|
+
if ( ! fs.existsSync( tmpNpmrc ) ) {
|
|
716
829
|
|
|
717
|
-
|
|
830
|
+
throw new CliError( 'Login failed: temp .npmrc not created.' );
|
|
718
831
|
|
|
719
|
-
|
|
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
|
-
|
|
840
|
+
const txt = fs.readFileSync( tmpNpmrc, 'utf8' );
|
|
841
|
+
const lines = txt.split( /\r?\n/ );
|
|
842
|
+
for ( const rawLine of lines ) {
|
|
724
843
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
848
|
+
const m = line.match( /^@[^:]+:registry=(.+)$/ );
|
|
849
|
+
if ( m && m[ 1 ] ) registryUrl = m[ 1 ].trim();
|
|
740
850
|
|
|
741
|
-
|
|
851
|
+
}
|
|
742
852
|
|
|
743
|
-
|
|
853
|
+
const authMatch = line.match( /^\/\/([^:]+):_authToken=.+$/ );
|
|
854
|
+
if ( authMatch && authMatch[ 1 ] ) {
|
|
744
855
|
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
-
|
|
865
|
+
logDebug( `Could not read temp .npmrc: ${readErr?.message || readErr}` );
|
|
781
866
|
|
|
782
|
-
|
|
783
|
-
starterVersion = info.version || '';
|
|
784
|
-
|
|
785
|
-
}
|
|
867
|
+
}
|
|
786
868
|
|
|
787
|
-
|
|
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
|
-
|
|
790
|
-
tarName = lines[ lines.length - 1 ] || '';
|
|
886
|
+
packedOut = exec( 'npm', packArgs, { cwd: tmp, env: createEnv } );
|
|
791
887
|
|
|
792
|
-
|
|
888
|
+
} catch ( err ) {
|
|
793
889
|
|
|
794
|
-
|
|
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
|
-
|
|
799
|
-
{
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
915
|
+
}
|
|
828
916
|
|
|
829
|
-
|
|
917
|
+
let tarName = '';
|
|
918
|
+
let starterVersion = '';
|
|
919
|
+
try {
|
|
830
920
|
|
|
831
|
-
|
|
921
|
+
const info = JSON.parse( packedOut );
|
|
922
|
+
if ( Array.isArray( info ) ) {
|
|
832
923
|
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
929
|
+
tarName = info.filename || '';
|
|
930
|
+
starterVersion = info.version || '';
|
|
847
931
|
|
|
848
932
|
}
|
|
849
933
|
|
|
850
|
-
}
|
|
934
|
+
} catch {
|
|
851
935
|
|
|
852
|
-
|
|
936
|
+
const lines = String( packedOut || '' ).trim().split( /\r?\n/ );
|
|
937
|
+
tarName = lines[ lines.length - 1 ] || '';
|
|
853
938
|
|
|
854
|
-
|
|
939
|
+
}
|
|
855
940
|
|
|
856
|
-
|
|
941
|
+
const tarPath = path.join( tmp, tarName );
|
|
942
|
+
if ( ! tarName || ! fs.existsSync( tarPath ) ) {
|
|
857
943
|
|
|
858
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
888
|
-
|
|
889
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1095
|
+
run( hasPnpm ? 'pnpm' : 'npm', [ hasPnpm ? 'install' : 'ci' ], {
|
|
1096
|
+
cwd: targetDir,
|
|
1097
|
+
env: sharedInstallEnv,
|
|
1098
|
+
} );
|
|
925
1099
|
|
|
926
|
-
|
|
1100
|
+
} catch ( err ) {
|
|
927
1101
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
1160
|
+
main()
|
|
1161
|
+
.catch( ( err ) => {
|
|
982
1162
|
|
|
983
|
-
|
|
1163
|
+
if ( err instanceof CliError ) {
|
|
984
1164
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1165
|
+
logError( err.message );
|
|
1166
|
+
const detail = ( err.stderr || err.stdout || '' ).trim();
|
|
1167
|
+
if ( detail ) {
|
|
988
1168
|
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
995
|
-
|
|
1177
|
+
console.error( dim( err.cause.stack ) );
|
|
1178
|
+
|
|
1179
|
+
}
|
|
996
1180
|
|
|
997
|
-
|
|
1181
|
+
process.exit( err.exitCode ?? 1 );
|
|
998
1182
|
|
|
999
1183
|
}
|
|
1000
1184
|
|
|
1001
|
-
|
|
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
|
-
|
|
1006
|
-
logError( fallback );
|
|
1007
|
-
process.exit( 1 );
|
|
1192
|
+
cleanupTmpDir();
|
|
1008
1193
|
|
|
1009
|
-
} );
|
|
1194
|
+
} );
|