create-three-blocks-starter 0.0.5 → 0.0.7
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/bin/index.js +937 -470
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -12,531 +12,998 @@ import { spawnSync } from 'node:child_process';
|
|
|
12
12
|
import readline from 'node:readline';
|
|
13
13
|
import { bold, cyan, dim, green, red, yellow } from 'kolorist';
|
|
14
14
|
|
|
15
|
-
const ESC = (n) => `\u001b[${n}m`;
|
|
16
|
-
const reset = ESC(0);
|
|
15
|
+
const ESC = ( n ) => `\u001b[${n}m`;
|
|
16
|
+
const reset = ESC( 0 );
|
|
17
17
|
|
|
18
|
-
const STARTER_PKG = '@three-blocks/starter';
|
|
19
|
-
const LOGIN_CLI = 'three-blocks-login';
|
|
18
|
+
const STARTER_PKG = '@three-blocks/starter'; // private starter (lives in CodeArtifact)
|
|
19
|
+
const LOGIN_CLI = 'three-blocks-login'; // public login CLI
|
|
20
|
+
const LOGIN_SPEC = `${LOGIN_CLI}@latest`; // force-fresh npx install
|
|
20
21
|
const SCOPE = '@three-blocks';
|
|
21
22
|
|
|
22
|
-
const BLOCKED_NPM_ENV_KEYS = new Set([
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
23
|
+
const BLOCKED_NPM_ENV_KEYS = new Set( [
|
|
24
|
+
'npm_config__three_blocks_registry',
|
|
25
|
+
'npm_config_verify_deps_before_run',
|
|
26
|
+
'npm_config_global_bin_dir',
|
|
27
|
+
'npm_config__jsr_registry',
|
|
28
|
+
'npm_config_node_linker',
|
|
29
|
+
].map( ( key ) => key.replace( /-/g, '_' ).toLowerCase() ) );
|
|
30
|
+
|
|
31
|
+
let DEBUG = /^1|true|yes$/i.test( String( process.env.THREE_BLOCKS_DEBUG || '' ).trim() );
|
|
32
|
+
|
|
33
|
+
const logDebug = ( msg ) => {
|
|
34
|
+
|
|
35
|
+
if ( DEBUG ) console.log( dim( `▸ ${msg}` ) );
|
|
36
|
+
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
class CliError extends Error {
|
|
40
|
+
|
|
41
|
+
constructor( message, {
|
|
42
|
+
exitCode = 1,
|
|
43
|
+
command = '',
|
|
44
|
+
args = [],
|
|
45
|
+
stdout = '',
|
|
46
|
+
stderr = '',
|
|
47
|
+
suggestion = '',
|
|
48
|
+
cause = undefined,
|
|
49
|
+
} = {} ) {
|
|
50
|
+
|
|
51
|
+
super( message );
|
|
52
|
+
this.name = 'CliError';
|
|
53
|
+
this.exitCode = exitCode;
|
|
54
|
+
this.command = command;
|
|
55
|
+
this.args = args;
|
|
56
|
+
this.stdout = stdout;
|
|
57
|
+
this.stderr = stderr;
|
|
58
|
+
this.suggestion = suggestion;
|
|
59
|
+
if ( cause ) this.cause = cause;
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const quoteArg = ( value ) => {
|
|
66
|
+
|
|
67
|
+
const str = String( value ?? '' );
|
|
68
|
+
if ( /^[A-Za-z0-9._-]+$/.test( str ) ) return str;
|
|
69
|
+
return `'${str.replace( /'/g, `'\\''` )}'`;
|
|
70
|
+
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const formatCommand = ( cmd, args = [] ) => [ cmd, ...args ].map( quoteArg ).join( ' ' );
|
|
74
|
+
|
|
75
|
+
const cleanNpmEnv = ( extra = {} ) => {
|
|
76
|
+
|
|
77
|
+
const env = { ...process.env, ...extra };
|
|
78
|
+
for ( const key of Object.keys( env ) ) {
|
|
79
|
+
|
|
80
|
+
const normalized = key.replace( /-/g, '_' ).toLowerCase();
|
|
81
|
+
if ( BLOCKED_NPM_ENV_KEYS.has( normalized ) ) {
|
|
82
|
+
|
|
83
|
+
delete env[ key ];
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return env;
|
|
90
|
+
|
|
44
91
|
};
|
|
45
92
|
|
|
46
93
|
const STEP_ICON = '⏺ ';
|
|
47
|
-
const logInfo = (msg) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
94
|
+
const logInfo = ( msg ) => {
|
|
95
|
+
|
|
96
|
+
console.log( `${STEP_ICON}${msg}` );
|
|
97
|
+
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const logProgress = ( msg ) => {
|
|
101
|
+
|
|
102
|
+
console.log( `${cyan( STEP_ICON )}${msg}` );
|
|
103
|
+
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const logSuccess = ( msg ) => {
|
|
107
|
+
|
|
108
|
+
console.log( `${green( STEP_ICON )}${msg}` );
|
|
109
|
+
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const logWarn = ( msg ) => {
|
|
113
|
+
|
|
114
|
+
console.log( `${yellow( STEP_ICON )}${msg}` );
|
|
115
|
+
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const logError = ( msg ) => {
|
|
119
|
+
|
|
120
|
+
console.error( `${red( STEP_ICON )}${msg}` );
|
|
121
|
+
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const die = ( m ) => {
|
|
125
|
+
|
|
126
|
+
throw new CliError( m );
|
|
127
|
+
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const run = ( cmd, args, opts = {} ) => {
|
|
131
|
+
|
|
132
|
+
const spawnArgs = Array.isArray( args ) ? args : [];
|
|
133
|
+
logDebug( `exec ${formatCommand( cmd, spawnArgs )}` );
|
|
134
|
+
const r = spawnSync( cmd, spawnArgs, { stdio: 'inherit', ...opts, env: cleanNpmEnv( opts.env ) } );
|
|
135
|
+
if ( r.error ) {
|
|
136
|
+
|
|
137
|
+
throw new CliError(
|
|
138
|
+
`Failed to start ${cmd}: ${r.error.message}`,
|
|
139
|
+
{ command: cmd, args: spawnArgs, cause: r.error }
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if ( r.status !== 0 ) {
|
|
145
|
+
|
|
146
|
+
throw new CliError(
|
|
147
|
+
`Command failed (${formatCommand( cmd, spawnArgs )}) [exit ${r.status}]`,
|
|
148
|
+
{ exitCode: r.status ?? 1, command: cmd, args: spawnArgs, stdout: r.stdout ? r.stdout.toString() : '' }
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return r;
|
|
154
|
+
|
|
57
155
|
};
|
|
58
156
|
|
|
59
157
|
// capture stdout (used for npm pack)
|
|
60
|
-
const exec = (cmd, args, opts = {}) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
158
|
+
const exec = ( cmd, args, opts = {} ) => {
|
|
159
|
+
|
|
160
|
+
const spawnArgs = Array.isArray( args ) ? args : [];
|
|
161
|
+
logDebug( `exec ${formatCommand( cmd, spawnArgs )}` );
|
|
162
|
+
const r = spawnSync( cmd, spawnArgs, { stdio: [ 'ignore', 'pipe', 'pipe' ], ...opts, env: cleanNpmEnv( opts.env ) } );
|
|
163
|
+
const stdout = r.stdout ? r.stdout.toString() : '';
|
|
164
|
+
const stderr = r.stderr ? r.stderr.toString() : '';
|
|
165
|
+
if ( r.error ) {
|
|
166
|
+
|
|
167
|
+
throw new CliError(
|
|
168
|
+
`Failed to start ${cmd}: ${r.error.message}`,
|
|
169
|
+
{ command: cmd, args: spawnArgs, stdout, stderr, cause: r.error }
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if ( r.status !== 0 ) {
|
|
175
|
+
|
|
176
|
+
throw new CliError(
|
|
177
|
+
`Command failed (${formatCommand( cmd, spawnArgs )}) [exit ${r.status}]`,
|
|
178
|
+
{ exitCode: r.status ?? 1, command: cmd, args: spawnArgs, stdout, stderr }
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if ( stderr.trim() ) logDebug( `stderr ${formatCommand( cmd, spawnArgs )}\n${stderr.trim()}` );
|
|
184
|
+
return stdout;
|
|
185
|
+
|
|
64
186
|
};
|
|
65
187
|
|
|
66
|
-
async function ensureEmptyDir(dir) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
188
|
+
async function ensureEmptyDir( dir ) {
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
|
|
192
|
+
await fsp.mkdir( dir, { recursive: true } );
|
|
193
|
+
|
|
194
|
+
} catch {}
|
|
195
|
+
|
|
196
|
+
const files = await fsp.readdir( dir ).catch( () => [] );
|
|
197
|
+
if ( files.length ) die( `Target directory '${path.basename( dir )}' is not empty.` );
|
|
198
|
+
|
|
70
199
|
}
|
|
71
200
|
|
|
72
|
-
async function promptHidden(label) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
201
|
+
async function promptHidden( label ) {
|
|
202
|
+
|
|
203
|
+
return await new Promise( ( resolve ) => {
|
|
204
|
+
|
|
205
|
+
const stdin = process.stdin;
|
|
206
|
+
const stdout = process.stdout;
|
|
207
|
+
let value = '';
|
|
208
|
+
const cleanup = () => {
|
|
209
|
+
|
|
210
|
+
stdin.removeListener( 'data', onData );
|
|
211
|
+
if ( stdin.isTTY ) stdin.setRawMode( false );
|
|
212
|
+
stdin.pause();
|
|
213
|
+
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const onData = ( chunk ) => {
|
|
217
|
+
|
|
218
|
+
const str = String( chunk );
|
|
219
|
+
for ( const ch of str ) {
|
|
220
|
+
|
|
221
|
+
if ( ch === '\u0003' ) {
|
|
222
|
+
|
|
223
|
+
cleanup(); process.exit( 1 );
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if ( ch === '\r' || ch === '\n' || ch === '\u0004' ) {
|
|
228
|
+
|
|
229
|
+
stdout.write( '\n' );
|
|
230
|
+
cleanup();
|
|
231
|
+
return resolve( value.trim() );
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if ( ch === '\u0008' || ch === '\u007f' ) {
|
|
236
|
+
|
|
237
|
+
if ( value.length ) {
|
|
238
|
+
|
|
239
|
+
value = value.slice( 0, - 1 );
|
|
240
|
+
readline.moveCursor( stdout, - 1, 0 );
|
|
241
|
+
stdout.write( ' ' );
|
|
242
|
+
readline.moveCursor( stdout, - 1, 0 );
|
|
243
|
+
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
continue;
|
|
247
|
+
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Ignore ESC start of control sequences
|
|
251
|
+
if ( ch === '\u001b' ) continue;
|
|
252
|
+
value += ch;
|
|
253
|
+
stdout.write( '•' );
|
|
254
|
+
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
stdout.write( label );
|
|
260
|
+
stdin.setEncoding( 'utf8' );
|
|
261
|
+
if ( stdin.isTTY ) stdin.setRawMode( true );
|
|
262
|
+
stdin.resume();
|
|
263
|
+
stdin.on( 'data', onData );
|
|
264
|
+
|
|
265
|
+
} );
|
|
266
|
+
|
|
112
267
|
}
|
|
113
268
|
|
|
114
|
-
function mkTmpDir(prefix = 'three-blocks-') {
|
|
115
|
-
|
|
269
|
+
function mkTmpDir( prefix = 'three-blocks-' ) {
|
|
270
|
+
|
|
271
|
+
return fs.mkdtempSync( path.join( os.tmpdir(), prefix ) );
|
|
272
|
+
|
|
116
273
|
}
|
|
117
274
|
|
|
118
|
-
function mask(s) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
275
|
+
function mask( s ) {
|
|
276
|
+
|
|
277
|
+
if ( ! s ) return '';
|
|
278
|
+
const v = String( s );
|
|
279
|
+
if ( v.length <= 6 ) return '••••';
|
|
280
|
+
return v.slice( 0, 3 ) + '••••' + v.slice( - 4 );
|
|
281
|
+
|
|
123
282
|
}
|
|
124
283
|
|
|
125
|
-
const BROKER_DEFAULT_ENDPOINT = process.env.THREE_BLOCKS_BROKER_URL || '
|
|
126
|
-
|
|
127
|
-
const resolveBrokerEndpoint = (channel) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
284
|
+
const BROKER_DEFAULT_ENDPOINT = process.env.THREE_BLOCKS_BROKER_URL || 'https://www.threejs-blocks.com/api/npm/token';
|
|
285
|
+
|
|
286
|
+
const resolveBrokerEndpoint = ( channel ) => {
|
|
287
|
+
|
|
288
|
+
let endpoint = BROKER_DEFAULT_ENDPOINT;
|
|
289
|
+
try {
|
|
290
|
+
|
|
291
|
+
const u = new URL( endpoint );
|
|
292
|
+
if ( channel && ! u.searchParams.get( 'channel' ) ) {
|
|
293
|
+
|
|
294
|
+
u.searchParams.set( 'channel', channel );
|
|
295
|
+
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
endpoint = u.toString();
|
|
299
|
+
|
|
300
|
+
} catch {}
|
|
301
|
+
|
|
302
|
+
return endpoint;
|
|
303
|
+
|
|
137
304
|
};
|
|
138
305
|
|
|
139
|
-
const fetchTokenMetadata = async (license, channel) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
306
|
+
const fetchTokenMetadata = async ( license, channel ) => {
|
|
307
|
+
|
|
308
|
+
if ( ! license || typeof fetch !== 'function' ) return null;
|
|
309
|
+
const endpoint = resolveBrokerEndpoint( channel );
|
|
310
|
+
try {
|
|
311
|
+
|
|
312
|
+
const res = await fetch( endpoint, {
|
|
313
|
+
method: 'GET',
|
|
314
|
+
headers: {
|
|
315
|
+
authorization: `Bearer ${license}`,
|
|
316
|
+
accept: 'application/json',
|
|
317
|
+
'x-three-blocks-channel': channel,
|
|
318
|
+
},
|
|
319
|
+
} );
|
|
320
|
+
if ( ! res.ok ) {
|
|
321
|
+
|
|
322
|
+
throw new Error( `HTTP ${res.status}` );
|
|
323
|
+
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return await res.json();
|
|
327
|
+
|
|
328
|
+
} catch ( err ) {
|
|
329
|
+
|
|
330
|
+
const msg = err instanceof Error ? err.message : String( err );
|
|
331
|
+
logWarn( `Continuing without token metadata (${msg})` );
|
|
332
|
+
return null;
|
|
333
|
+
|
|
334
|
+
}
|
|
335
|
+
|
|
160
336
|
};
|
|
161
337
|
|
|
162
|
-
const formatRegistryShort = (value) => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
338
|
+
const formatRegistryShort = ( value ) => {
|
|
339
|
+
|
|
340
|
+
if ( ! value ) return '';
|
|
341
|
+
try {
|
|
342
|
+
|
|
343
|
+
const u = new URL( value );
|
|
344
|
+
const pathname = ( u.pathname || '' ).replace( /\/$/, '' );
|
|
345
|
+
return `${u.host}${pathname}`;
|
|
346
|
+
|
|
347
|
+
} catch {
|
|
348
|
+
|
|
349
|
+
return String( value );
|
|
350
|
+
|
|
351
|
+
}
|
|
352
|
+
|
|
171
353
|
};
|
|
172
354
|
|
|
173
|
-
const formatExpiryLabel = (iso) => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
355
|
+
const formatExpiryLabel = ( iso ) => {
|
|
356
|
+
|
|
357
|
+
if ( ! iso ) return 'Expires: —';
|
|
358
|
+
const dt = new Date( iso );
|
|
359
|
+
if ( Number.isNaN( dt.getTime() ) ) return `Expires: ${iso}`;
|
|
360
|
+
return `Expires: ${dt.toISOString().replace( 'T', ' ' ).replace( 'Z', 'Z' )}`;
|
|
361
|
+
|
|
178
362
|
};
|
|
179
363
|
|
|
180
364
|
const HEADER_WIDTH = 118;
|
|
181
365
|
const LEFT_WIDTH = 72;
|
|
182
366
|
const RIGHT_WIDTH = HEADER_WIDTH - LEFT_WIDTH - 3;
|
|
183
367
|
|
|
184
|
-
const repeatChar = (ch, len) => ch.repeat(Math.max(0, len));
|
|
185
|
-
const stripAnsi = (value) => String(value ?? '').replace(/\u001b\[[0-9;]*m/g, '');
|
|
186
|
-
const ellipsize = (value, width) => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
368
|
+
const repeatChar = ( ch, len ) => ch.repeat( Math.max( 0, len ) );
|
|
369
|
+
const stripAnsi = ( value ) => String( value ?? '' ).replace( /\u001b\[[0-9;]*m/g, '' );
|
|
370
|
+
const ellipsize = ( value, width ) => {
|
|
371
|
+
|
|
372
|
+
const str = String( value ?? '' );
|
|
373
|
+
const plain = stripAnsi( str );
|
|
374
|
+
if ( plain.length <= width ) return str;
|
|
375
|
+
if ( width <= 1 ) return plain.slice( 0, width );
|
|
376
|
+
const truncated = plain.slice( 0, width - 1 ) + '…';
|
|
377
|
+
return plain === str ? truncated : truncated;
|
|
378
|
+
|
|
193
379
|
};
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
380
|
+
|
|
381
|
+
const visibleLength = ( value ) => stripAnsi( String( value ?? '' ) ).length;
|
|
382
|
+
const padText = ( value, width, align = 'left' ) => {
|
|
383
|
+
|
|
384
|
+
const text = ellipsize( value, width );
|
|
385
|
+
const remaining = width - visibleLength( text );
|
|
386
|
+
if ( remaining <= 0 ) return text;
|
|
387
|
+
if ( align === 'center' ) {
|
|
388
|
+
|
|
389
|
+
const left = Math.floor( remaining / 2 );
|
|
390
|
+
const right = remaining - left;
|
|
391
|
+
return `${' '.repeat( left )}${text}${' '.repeat( right )}`;
|
|
392
|
+
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if ( align === 'right' ) {
|
|
396
|
+
|
|
397
|
+
return `${' '.repeat( remaining )}${text}`;
|
|
398
|
+
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return `${text}${' '.repeat( remaining )}`;
|
|
402
|
+
|
|
208
403
|
};
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
404
|
+
|
|
405
|
+
const makeHeaderRow = ( left, right = '', leftAlign = 'left', rightAlign = 'left' ) =>
|
|
406
|
+
`│${padText( left, LEFT_WIDTH, leftAlign )}│${padText( right, RIGHT_WIDTH, rightAlign )}│`;
|
|
407
|
+
|
|
408
|
+
const HEADER_COLOR = ESC( 33 );
|
|
409
|
+
const CONTENT_COLOR = ESC( 90 );
|
|
410
|
+
const reapplyColor = ( value, color ) => {
|
|
411
|
+
|
|
412
|
+
const str = String( value ?? '' );
|
|
413
|
+
return `${color}${str.split( reset ).join( `${reset}${color}` )}${reset}`;
|
|
414
|
+
|
|
217
415
|
};
|
|
218
416
|
|
|
219
|
-
const applyHeaderColor = (row, { keepContentYellow = false, tintContent = true } = {}) => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
417
|
+
const applyHeaderColor = ( row, { keepContentYellow = false, tintContent = true } = {} ) => {
|
|
418
|
+
|
|
419
|
+
if ( keepContentYellow ) return reapplyColor( row, HEADER_COLOR );
|
|
420
|
+
if ( ! row.startsWith( '│' ) || ! row.endsWith( '│' ) ) return reapplyColor( row, HEADER_COLOR );
|
|
421
|
+
const match = row.match( /^│(.*)│(.*)│$/ );
|
|
422
|
+
if ( ! match ) return reapplyColor( row, HEADER_COLOR );
|
|
423
|
+
const [ , leftContent, rightContent ] = match;
|
|
424
|
+
const leftSegment = tintContent ? reapplyColor( leftContent, CONTENT_COLOR ) : leftContent;
|
|
425
|
+
const rightSegment = tintContent ? reapplyColor( rightContent, CONTENT_COLOR ) : rightContent;
|
|
426
|
+
return `${HEADER_COLOR}│${reset}${leftSegment}${HEADER_COLOR}│${reset}${rightSegment}${HEADER_COLOR}│${reset}`;
|
|
427
|
+
|
|
228
428
|
};
|
|
229
429
|
|
|
230
|
-
const capitalize = (value) => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
430
|
+
const capitalize = ( value ) => {
|
|
431
|
+
|
|
432
|
+
const str = String( value || '' ).trim();
|
|
433
|
+
if ( ! str ) return '';
|
|
434
|
+
return str[ 0 ].toUpperCase() + str.slice( 1 );
|
|
435
|
+
|
|
234
436
|
};
|
|
235
437
|
|
|
236
|
-
const formatPlanLabel = (value) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
438
|
+
const formatPlanLabel = ( value ) => {
|
|
439
|
+
|
|
440
|
+
const str = String( value || '' ).trim();
|
|
441
|
+
if ( ! str ) return '';
|
|
442
|
+
return str
|
|
443
|
+
.split( /\s+/ )
|
|
444
|
+
.map( ( part ) => ( part ? capitalize( part.toLowerCase() ) : '' ) )
|
|
445
|
+
.join( ' ' )
|
|
446
|
+
.trim();
|
|
447
|
+
|
|
244
448
|
};
|
|
245
449
|
|
|
246
|
-
const formatFirstName = (value) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
450
|
+
const formatFirstName = ( value ) => {
|
|
451
|
+
|
|
452
|
+
const name = String( value || '' ).trim();
|
|
453
|
+
if ( ! name ) return '';
|
|
454
|
+
const first = name.split( /\s+/ )[ 0 ];
|
|
455
|
+
return capitalize( first );
|
|
456
|
+
|
|
251
457
|
};
|
|
252
458
|
|
|
253
459
|
const getUserDisplayName = () => {
|
|
254
|
-
|
|
460
|
+
|
|
461
|
+
const candidate = process.env.THREE_BLOCKS_USER_NAME
|
|
255
462
|
|| process.env.GIT_AUTHOR_NAME
|
|
256
463
|
|| process.env.USER
|
|
257
464
|
|| process.env.LOGNAME;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
465
|
+
if ( candidate ) return formatFirstName( candidate );
|
|
466
|
+
try {
|
|
467
|
+
|
|
468
|
+
return formatFirstName( os.userInfo().username );
|
|
469
|
+
|
|
470
|
+
} catch {
|
|
471
|
+
|
|
472
|
+
return '';
|
|
473
|
+
|
|
474
|
+
}
|
|
475
|
+
|
|
264
476
|
};
|
|
265
477
|
|
|
266
|
-
const inferPlan = (license) => {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
478
|
+
const inferPlan = ( license ) => {
|
|
479
|
+
|
|
480
|
+
if ( ! license ) return 'Unknown plan';
|
|
481
|
+
if ( /live/i.test( license ) ) return 'Pro Plan';
|
|
482
|
+
if ( /test/i.test( license ) ) return 'Sandbox Plan';
|
|
483
|
+
if ( /beta/i.test( license ) ) return 'Beta Plan';
|
|
484
|
+
return 'Developer Plan';
|
|
485
|
+
|
|
272
486
|
};
|
|
273
487
|
|
|
274
|
-
const extractVersionFromTarball = (filename) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
488
|
+
const extractVersionFromTarball = ( filename ) => {
|
|
489
|
+
|
|
490
|
+
if ( ! filename ) return '';
|
|
491
|
+
const match = filename.match( /-(\d+\.\d+\.\d+(?:-[^.]*)?)\.tgz$/ );
|
|
492
|
+
return match ? match[ 1 ] : '';
|
|
493
|
+
|
|
278
494
|
};
|
|
279
495
|
|
|
280
|
-
const renderHeader = ({
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}) => {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
496
|
+
const renderHeader = ( {
|
|
497
|
+
starterVersion,
|
|
498
|
+
userDisplayName,
|
|
499
|
+
planName,
|
|
500
|
+
repoPath,
|
|
501
|
+
projectName,
|
|
502
|
+
channel,
|
|
503
|
+
coreVersion,
|
|
504
|
+
license,
|
|
505
|
+
registry,
|
|
506
|
+
domain,
|
|
507
|
+
region,
|
|
508
|
+
repository,
|
|
509
|
+
expiresAt,
|
|
510
|
+
teamId,
|
|
511
|
+
teamName,
|
|
512
|
+
licenseId,
|
|
513
|
+
} ) => {
|
|
514
|
+
|
|
515
|
+
const welcome = userDisplayName ? `Welcome back ${userDisplayName}!` : 'Welcome!';
|
|
516
|
+
const normalizedPlan = planName || 'Unknown plan';
|
|
517
|
+
const resolvedTeamName = teamName || teamId || '';
|
|
518
|
+
const planSuffix = resolvedTeamName ? ` · Team: ${resolvedTeamName}` : '';
|
|
519
|
+
const planLine = `@three-blocks/core ${coreVersion || ( channel === 'stable' ? 'latest' : channel )} · ${normalizedPlan}${planSuffix}`;
|
|
520
|
+
const channelDisplay = ( channel || 'stable' ).toUpperCase();
|
|
521
|
+
const channelLine = `Channel: ${channelDisplay}${region ? ` · Region: ${region}` : ''}`;
|
|
522
|
+
const repositoryLineBase = repository ? `Repository: ${repository}` : 'Repository: —';
|
|
523
|
+
const registryShort = formatRegistryShort( registry );
|
|
524
|
+
const repositoryLine = registryShort ? `${repositoryLineBase} → ${registryShort}` : repositoryLineBase;
|
|
525
|
+
const registryLine = `Registry: ${registryShort || '—'}`;
|
|
526
|
+
const licenseLine = `License: ${mask( license )}${licenseId ? ` · ${licenseId}` : ''}`;
|
|
527
|
+
const expiresLine = formatExpiryLabel( expiresAt );
|
|
528
|
+
const projectLabel = projectName || path.basename( repoPath );
|
|
529
|
+
const projectLine = `Project: ${projectLabel}`;
|
|
530
|
+
const domainLine = domain ? `Domain: ${domain}` : '';
|
|
531
|
+
const regionLine = region ? `Region: ${region}` : '';
|
|
532
|
+
const title = `─── Three.js Blocks Starter v${starterVersion || '?.?.?'} `;
|
|
533
|
+
const separatorRow = makeHeaderRow( repeatChar( '─', LEFT_WIDTH ), repeatChar( '─', RIGHT_WIDTH ) );
|
|
534
|
+
const ascii = [
|
|
535
|
+
'THREE.JS',
|
|
536
|
+
' ______ __ ______ ______ __ __ ______ ',
|
|
537
|
+
'/\\ == \\ /\\ \\ /\\ __ \\ /\\ ___\\ /\\ \\/ / /\\ ___\\ ',
|
|
538
|
+
'\\ \\ __< \\ \\ \\____ \\ \\ \\/\\ \\ \\ \\ \\____ \\ \\ _"-. \\ \\___ \\ ',
|
|
539
|
+
' \\ \\_____\\ \\ \\_____\\ \\ \\_____\\ \\ \\_____\\ \\ \\_\\ \\_\\ \\/\\_____\\',
|
|
540
|
+
' \\/_____\/ \\/_____/ \\/_____/ \\/_____/ \\/_/ \/_/ \\/_____/'
|
|
541
|
+
];
|
|
542
|
+
const lines = [
|
|
543
|
+
applyHeaderColor( `╭${title}${repeatChar( '─', HEADER_WIDTH - 2 - title.length )}╮`, { keepContentYellow: true } ),
|
|
544
|
+
...ascii.map( ( row ) => applyHeaderColor( `│${padText( row, LEFT_WIDTH + RIGHT_WIDTH + 1, 'center' )}│`, { keepContentYellow: true } ) ),
|
|
545
|
+
applyHeaderColor( separatorRow, { keepContentYellow: true } ),
|
|
546
|
+
applyHeaderColor( makeHeaderRow( welcome, projectLine, 'center', 'center' ) ),
|
|
547
|
+
applyHeaderColor( separatorRow, { keepContentYellow: true } ),
|
|
548
|
+
applyHeaderColor( makeHeaderRow( planLine, channelLine ) ),
|
|
549
|
+
applyHeaderColor( makeHeaderRow( repositoryLine, registryLine ) ),
|
|
550
|
+
...( domainLine || regionLine ? [ applyHeaderColor( makeHeaderRow( domainLine || '', regionLine, 'left', 'center' ) ) ] : [] ),
|
|
551
|
+
applyHeaderColor( makeHeaderRow( licenseLine, expiresLine ) ),
|
|
552
|
+
applyHeaderColor( `╰${repeatChar( '─', HEADER_WIDTH - 2 )}╯`, { keepContentYellow: true } ),
|
|
553
|
+
];
|
|
554
|
+
for ( const row of lines ) console.log( row );
|
|
555
|
+
|
|
338
556
|
};
|
|
339
557
|
|
|
340
558
|
async function main() {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
559
|
+
|
|
560
|
+
const args = process.argv.slice( 2 );
|
|
561
|
+
let appName = '';
|
|
562
|
+
let channel = String( process.env.THREE_BLOCKS_CHANNEL || 'stable' ).toLowerCase();
|
|
563
|
+
let debug = DEBUG;
|
|
564
|
+
const userDisplayName = getUserDisplayName();
|
|
565
|
+
const repoPath = process.cwd();
|
|
566
|
+
for ( let i = 0; i < args.length; i ++ ) {
|
|
567
|
+
|
|
568
|
+
const a = args[ i ];
|
|
569
|
+
if ( ! a.startsWith( '-' ) && ! appName ) {
|
|
570
|
+
|
|
571
|
+
appName = a; continue;
|
|
572
|
+
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if ( a === '--debug' || a === '-d' ) {
|
|
576
|
+
|
|
577
|
+
debug = true; continue;
|
|
578
|
+
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if ( a === '--channel' || a === '-c' ) {
|
|
582
|
+
|
|
583
|
+
const v = args[ i + 1 ]; if ( v ) {
|
|
584
|
+
|
|
585
|
+
channel = String( v ).toLowerCase(); i ++;
|
|
586
|
+
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
continue;
|
|
590
|
+
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const m = a.match( /^--channel=(.+)$/ );
|
|
594
|
+
if ( m ) {
|
|
595
|
+
|
|
596
|
+
channel = String( m[ 1 ] ).toLowerCase(); continue;
|
|
597
|
+
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
DEBUG = debug;
|
|
603
|
+
if ( DEBUG ) logDebug( 'Debug logging enabled.' );
|
|
604
|
+
|
|
605
|
+
if ( ! [ 'stable', 'beta', 'alpha' ].includes( channel ) ) channel = 'stable';
|
|
606
|
+
if ( ! appName ) {
|
|
607
|
+
|
|
608
|
+
die(
|
|
609
|
+
`Usage:\n` +
|
|
610
|
+
` THREE_BLOCKS_SECRET_KEY=sk_live_... npx create-three-blocks-starter <directory> [--channel <stable|beta|alpha>]\n` +
|
|
611
|
+
` npx create-three-blocks-starter <directory> (will prompt for the license key)\n` +
|
|
612
|
+
` Add --debug for verbose logging.`
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const targetDir = path.resolve( process.cwd(), appName );
|
|
618
|
+
await ensureEmptyDir( targetDir );
|
|
619
|
+
|
|
620
|
+
// 1) License key (env or prompt)
|
|
621
|
+
let license = process.env.THREE_BLOCKS_SECRET_KEY;
|
|
622
|
+
if ( license ) {
|
|
623
|
+
|
|
624
|
+
logInfo( `Using ${bold( 'THREE_BLOCKS_SECRET_KEY' )} from environment ${dim( `(${mask( license )})` )}` );
|
|
625
|
+
|
|
626
|
+
} else {
|
|
627
|
+
|
|
628
|
+
console.log( '' );
|
|
629
|
+
logInfo( bold( cyan( 'Three Blocks Starter' ) ) + ' ' + dim( `[channel: ${channel}]` ) );
|
|
630
|
+
logInfo( dim( 'Enter your license key to continue.' ) );
|
|
631
|
+
logInfo( dim( 'Tip: paste it here; input is hidden. Press Enter to submit.' ) );
|
|
632
|
+
license = await promptHidden( `${STEP_ICON}${bold( 'License key' )} ${dim( '(tb_…)' )}: ` );
|
|
633
|
+
if ( ! license ) die( 'License key is required to install private packages.' );
|
|
634
|
+
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let planName = inferPlan( license );
|
|
638
|
+
const tokenMetadata = await fetchTokenMetadata( license, channel );
|
|
639
|
+
let headerChannel = channel;
|
|
640
|
+
if ( tokenMetadata?.channel ) {
|
|
641
|
+
|
|
642
|
+
const maybeChannel = String( tokenMetadata.channel ).toLowerCase();
|
|
643
|
+
if ( [ 'stable', 'beta', 'alpha' ].includes( maybeChannel ) ) headerChannel = maybeChannel;
|
|
644
|
+
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if ( tokenMetadata?.plan ) {
|
|
648
|
+
|
|
649
|
+
const label = formatPlanLabel( tokenMetadata.plan );
|
|
650
|
+
if ( label ) {
|
|
651
|
+
|
|
652
|
+
planName = label.toLowerCase().includes( 'plan' ) ? label : `${label} Plan`;
|
|
653
|
+
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
let headerRegistry = tokenMetadata?.registry ? String( tokenMetadata.registry ) : '';
|
|
659
|
+
const headerDomain = tokenMetadata?.domain ? String( tokenMetadata.domain ) : '';
|
|
660
|
+
const headerRegion = tokenMetadata?.region ? String( tokenMetadata.region ) : '';
|
|
661
|
+
const headerRepository = tokenMetadata?.repository ? String( tokenMetadata.repository ) : '';
|
|
662
|
+
const headerExpires = tokenMetadata?.expiresAt ?? tokenMetadata?.expiresAtIso ?? null;
|
|
663
|
+
const headerTeamId = tokenMetadata?.teamId ? String( tokenMetadata.teamId ) : '';
|
|
664
|
+
const headerTeamName = tokenMetadata?.teamName ? String( tokenMetadata.teamName ) : '';
|
|
665
|
+
const headerLicenseId = tokenMetadata?.licenseId ? String( tokenMetadata.licenseId ) : '';
|
|
666
|
+
|
|
667
|
+
// 2) Pre-login in a TEMP dir to generate a temp .npmrc (no always-auth)
|
|
668
|
+
let tmp = '';
|
|
669
|
+
let tmpNpmrc = '';
|
|
670
|
+
try {
|
|
671
|
+
|
|
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 ) {
|
|
685
|
+
|
|
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
|
+
);
|
|
696
|
+
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
throw err;
|
|
700
|
+
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if ( ! fs.existsSync( tmpNpmrc ) ) {
|
|
704
|
+
|
|
705
|
+
throw new CliError( 'Login failed: temp .npmrc not created.' );
|
|
706
|
+
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Extract registry URL from temp .npmrc so we can pass it explicitly to npm create
|
|
710
|
+
let registryUrl = '';
|
|
711
|
+
try {
|
|
712
|
+
|
|
713
|
+
const txt = fs.readFileSync( tmpNpmrc, 'utf8' );
|
|
714
|
+
const m = txt.match( /^@[^:]+:registry=(.+)$/m );
|
|
715
|
+
if ( m && m[ 1 ] ) registryUrl = m[ 1 ].trim();
|
|
716
|
+
|
|
717
|
+
} catch ( readErr ) {
|
|
718
|
+
|
|
719
|
+
logDebug( `Could not read temp .npmrc: ${readErr?.message || readErr}` );
|
|
720
|
+
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if ( ! headerRegistry && registryUrl ) headerRegistry = registryUrl;
|
|
724
|
+
|
|
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 {
|
|
738
|
+
|
|
739
|
+
packedOut = exec( 'npm', packArgs, { cwd: tmp, env: createEnv } );
|
|
740
|
+
|
|
741
|
+
} catch ( err ) {
|
|
742
|
+
|
|
743
|
+
if ( err instanceof CliError ) {
|
|
744
|
+
|
|
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
|
+
);
|
|
760
|
+
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
throw new CliError(
|
|
764
|
+
`Failed to fetch starter tarball ${starterSpec}.`,
|
|
765
|
+
{ cause: err }
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
}
|
|
769
|
+
|
|
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 || '';
|
|
779
|
+
|
|
780
|
+
} else {
|
|
781
|
+
|
|
782
|
+
tarName = info.filename || '';
|
|
783
|
+
starterVersion = info.version || '';
|
|
784
|
+
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
} catch {
|
|
788
|
+
|
|
789
|
+
const lines = String( packedOut || '' ).trim().split( /\r?\n/ );
|
|
790
|
+
tarName = lines[ lines.length - 1 ] || '';
|
|
791
|
+
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const tarPath = path.join( tmp, tarName );
|
|
795
|
+
if ( ! tarName || ! fs.existsSync( tarPath ) ) {
|
|
796
|
+
|
|
797
|
+
throw new CliError(
|
|
798
|
+
`Failed to fetch starter tarball ${starterSpec}.`,
|
|
799
|
+
{ suggestion: 'Ensure the requested release is published and accessible for your channel.' }
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
}
|
|
803
|
+
|
|
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 {
|
|
826
|
+
|
|
827
|
+
run( 'tar', [ '-xzf', tarPath, '-C', targetDir, '--strip-components=1' ] );
|
|
828
|
+
|
|
829
|
+
} catch ( err ) {
|
|
830
|
+
|
|
831
|
+
if ( err instanceof CliError ) {
|
|
832
|
+
|
|
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
|
+
);
|
|
843
|
+
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
throw err;
|
|
847
|
+
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
} finally {
|
|
851
|
+
|
|
852
|
+
if ( tmp ) {
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
|
|
856
|
+
fs.rmSync( tmp, { recursive: true, force: true } );
|
|
857
|
+
|
|
858
|
+
} catch ( cleanupErr ) {
|
|
859
|
+
|
|
860
|
+
logDebug( `Unable to remove temp directory ${tmp}: ${cleanupErr?.message || cleanupErr}` );
|
|
861
|
+
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// 4) Write .env.local and .gitignore entries
|
|
869
|
+
await fsp.writeFile( path.join( targetDir, '.env.local' ),
|
|
870
|
+
`THREE_BLOCKS_SECRET_KEY=${license}\nTHREE_BLOCKS_CHANNEL=${channel}\n`, { mode: 0o600 } ).catch( () => {} );
|
|
871
|
+
const giPath = path.join( targetDir, '.gitignore' );
|
|
872
|
+
let gi = '';
|
|
873
|
+
try {
|
|
874
|
+
|
|
875
|
+
gi = await fsp.readFile( giPath, 'utf8' );
|
|
876
|
+
|
|
877
|
+
} catch {}
|
|
878
|
+
|
|
879
|
+
for ( const line of [ '.env.local', '.npmrc' ] ) {
|
|
880
|
+
|
|
881
|
+
if ( ! gi.includes( line ) ) gi += ( gi.endsWith( '\n' ) || gi === '' ? '' : '\n' ) + line + '\n';
|
|
882
|
+
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
await fsp.writeFile( giPath, gi );
|
|
886
|
+
|
|
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.
|
|
890
|
+
try {
|
|
891
|
+
|
|
892
|
+
const pkgPath = path.join( targetDir, 'package.json' );
|
|
893
|
+
const pkgRaw = await fsp.readFile( pkgPath, 'utf8' );
|
|
894
|
+
const pkg = JSON.parse( pkgRaw );
|
|
895
|
+
pkg.scripts = pkg.scripts || {};
|
|
896
|
+
if ( ! pkg.scripts.preinstall ) {
|
|
897
|
+
|
|
898
|
+
pkg.scripts.preinstall = `npx -y ${LOGIN_CLI}@latest --mode project --scope ${SCOPE} --channel ${channel}`;
|
|
899
|
+
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
await fsp.writeFile( pkgPath, JSON.stringify( pkg, null, 2 ) );
|
|
903
|
+
logSuccess( 'Added preinstall to generated package.json to refresh token on installs.' );
|
|
904
|
+
|
|
905
|
+
} catch ( e ) {
|
|
906
|
+
|
|
907
|
+
logWarn( `Warning: could not add preinstall to package.json: ${e?.message || String( e )}` );
|
|
908
|
+
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
logProgress( 'Installing dependencies (preinstall will refresh token) ...' );
|
|
912
|
+
const hasPnpm = spawnSync( 'pnpm', [ '-v' ], { stdio: 'ignore', env: cleanNpmEnv() } ).status === 0;
|
|
913
|
+
try {
|
|
914
|
+
|
|
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
|
+
} );
|
|
923
|
+
|
|
924
|
+
} catch ( err ) {
|
|
925
|
+
|
|
926
|
+
if ( err instanceof CliError ) {
|
|
927
|
+
|
|
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
|
+
);
|
|
940
|
+
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
throw err;
|
|
944
|
+
|
|
945
|
+
}
|
|
946
|
+
|
|
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 ) {
|
|
961
|
+
|
|
962
|
+
logWarn( `Warning: could not install @three-blocks/core (exit ${r.status}).` );
|
|
963
|
+
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
console.log( '' );
|
|
969
|
+
logSuccess( `${appName} is ready.` );
|
|
970
|
+
console.log( '' );
|
|
971
|
+
logInfo( 'Next:' );
|
|
972
|
+
console.log( ` cd ${appName}` );
|
|
973
|
+
console.log( ` ${hasPnpm ? 'pnpm dev' : 'npm run dev'}` );
|
|
974
|
+
console.log( '' );
|
|
975
|
+
logInfo( 'Notes:' );
|
|
976
|
+
console.log( ` • Your license key is stored in .env.local (gitignored).` );
|
|
977
|
+
console.log( ` • ${SCOPE} packages are private; each install refreshes a short-lived token via preinstall.` );
|
|
978
|
+
|
|
537
979
|
}
|
|
538
980
|
|
|
539
|
-
main().catch((
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
981
|
+
main().catch( ( err ) => {
|
|
982
|
+
|
|
983
|
+
if ( err instanceof CliError ) {
|
|
984
|
+
|
|
985
|
+
logError( err.message );
|
|
986
|
+
const detail = ( err.stderr || err.stdout || '' ).trim();
|
|
987
|
+
if ( detail ) {
|
|
988
|
+
|
|
989
|
+
const lines = detail.split( /\r?\n/ );
|
|
990
|
+
for ( const line of lines ) console.error( dim( ` ${line}` ) );
|
|
991
|
+
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if ( err.suggestion ) logInfo( dim( `Hint: ${err.suggestion}` ) );
|
|
995
|
+
if ( DEBUG && err.cause && err.cause !== err && err.cause?.stack ) {
|
|
996
|
+
|
|
997
|
+
console.error( dim( err.cause.stack ) );
|
|
998
|
+
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
process.exit( err.exitCode ?? 1 );
|
|
1002
|
+
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const fallback = err?.stack || err?.message || String( err );
|
|
1006
|
+
logError( fallback );
|
|
1007
|
+
process.exit( 1 );
|
|
1008
|
+
|
|
1009
|
+
} );
|