three-blocks-login 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/login.js +900 -493
  2. package/package.json +1 -1
package/bin/login.js CHANGED
@@ -8,573 +8,980 @@ import path from "node:path";
8
8
  import readline from "node:readline";
9
9
  import { createRequire } from "node:module";
10
10
 
11
- const require = createRequire(import.meta.url);
12
- const pkg = require("../package.json");
13
- const CLI_VERSION = pkg?.version ? String(pkg.version) : "?.?.?";
11
+ const require = createRequire( import.meta.url );
12
+ const pkg = require( "../package.json" );
13
+ const CLI_VERSION = pkg?.version ? String( pkg.version ) : "?.?.?";
14
14
 
15
15
  // Simple ANSI color helpers (no deps)
16
- const ESC = (n) => `\u001b[${n}m`;
17
- const reset = ESC(0);
18
- const bold = (s) => ESC(1) + s + reset;
19
- const dim = (s) => ESC(2) + s + reset;
20
- const red = (s) => ESC(31) + s + reset;
21
- const green = (s) => ESC(32) + s + reset;
22
- const yellow = (s) => ESC(33) + s + reset;
23
- const cyan = (s) => ESC(36) + s + reset;
16
+ const ESC = ( n ) => `\u001b[${n}m`;
17
+ const reset = ESC( 0 );
18
+ const bold = ( s ) => ESC( 1 ) + s + reset;
19
+ const dim = ( s ) => ESC( 2 ) + s + reset;
20
+ const red = ( s ) => ESC( 31 ) + s + reset;
21
+ const green = ( s ) => ESC( 32 ) + s + reset;
22
+ const yellow = ( s ) => ESC( 33 ) + s + reset;
23
+ const cyan = ( s ) => ESC( 36 ) + s + reset;
24
24
  const plainBanner = `[three-blocks-login@${pkg.version}]`;
25
- const banner = cyan(plainBanner);
25
+ const banner = cyan( plainBanner );
26
26
 
27
- const quoteShell = (value) => JSON.stringify(String(value ?? ''));
27
+ const quoteShell = ( value ) => JSON.stringify( String( value ?? '' ) );
28
+
29
+ const BLOCKED_NPM_ENV_KEYS = new Set(
30
+ [
31
+ "npm_config__three_blocks_registry",
32
+ "npm_config_verify_deps_before_run",
33
+ "npm_config_global_bin_dir",
34
+ "npm_config__jsr_registry",
35
+ "npm_config_node_linker",
36
+ ].map( ( key ) => key.replace( /-/g, "_" ).toLowerCase() )
37
+ );
38
+
39
+ const scrubNpmEnv = ( env = process.env ) => {
40
+
41
+ let removed = 0;
42
+ for ( const key of Object.keys( env ) ) {
43
+
44
+ const normalized = key.replace( /-/g, "_" ).toLowerCase();
45
+ if ( BLOCKED_NPM_ENV_KEYS.has( normalized ) ) {
46
+
47
+ delete env[ key ];
48
+ removed ++;
49
+
50
+ }
51
+
52
+ }
53
+
54
+ return removed;
55
+
56
+ };
57
+
58
+ scrubNpmEnv();
59
+
60
+ let DEBUG = /^1|true|yes$/i.test( String( process.env.THREE_BLOCKS_DEBUG || "" ).trim() );
61
+
62
+ const logDebug = ( msg ) => {
63
+
64
+ if ( DEBUG ) console.error( dim( `[debug] ${msg}` ) );
65
+
66
+ };
67
+
68
+ class CliError extends Error {
69
+
70
+ constructor( message, {
71
+ exitCode = 1,
72
+ command = "",
73
+ args = [],
74
+ stdout = "",
75
+ stderr = "",
76
+ suggestion = "",
77
+ cause = undefined,
78
+ } = {} ) {
79
+
80
+ super( message );
81
+ this.name = "CliError";
82
+ this.exitCode = exitCode;
83
+ this.command = command;
84
+ this.args = args;
85
+ this.stdout = stdout;
86
+ this.stderr = stderr;
87
+ this.suggestion = suggestion;
88
+ if ( cause ) this.cause = cause;
89
+
90
+ }
91
+
92
+ }
93
+
94
+ const quoteArg = ( value ) => {
95
+
96
+ const str = String( value ?? "" );
97
+ if ( /^[A-Za-z0-9._-]+$/.test( str ) ) return str;
98
+ return `'${str.replace( /'/g, `'\\''` )}'`;
99
+
100
+ };
101
+
102
+ const formatCommand = ( cmd, args = [] ) => [ cmd, ...args ].map( quoteArg ).join( " " );
28
103
 
29
104
  const HEADER_WIDTH = 96;
30
105
  const LEFT_WIDTH = 60;
31
106
  const RIGHT_WIDTH = HEADER_WIDTH - LEFT_WIDTH - 3;
32
107
 
33
- const repeatChar = (ch, len) => ch.repeat(Math.max(0, len));
34
- const stripAnsi = (value) => String(value ?? '').replace(/\u001b\[[0-9;]*m/g, '');
35
- const ellipsize = (value, width) => {
36
- const str = String(value ?? '');
37
- const plain = stripAnsi(str);
38
- if (plain.length <= width) return str;
39
- if (width <= 1) return plain.slice(0, width);
40
- const truncated = plain.slice(0, width - 1) + '…';
41
- return plain === str ? truncated : truncated;
108
+ const repeatChar = ( ch, len ) => ch.repeat( Math.max( 0, len ) );
109
+ const stripAnsi = ( value ) => String( value ?? '' ).replace( /\u001b\[[0-9;]*m/g, '' );
110
+ const ellipsize = ( value, width ) => {
111
+
112
+ const str = String( value ?? '' );
113
+ const plain = stripAnsi( str );
114
+ if ( plain.length <= width ) return str;
115
+ if ( width <= 1 ) return plain.slice( 0, width );
116
+ const truncated = plain.slice( 0, width - 1 ) + '…';
117
+ return plain === str ? truncated : truncated;
118
+
42
119
  };
43
- const visibleLength = (value) => stripAnsi(String(value ?? '')).length;
44
- const padText = (value, width, align = 'left') => {
45
- const text = ellipsize(value, width);
46
- const current = visibleLength(text);
47
- const remaining = width - current;
48
- if (remaining <= 0) return text;
49
- if (align === 'center') {
50
- const left = Math.floor(remaining / 2);
51
- const right = remaining - left;
52
- return `${' '.repeat(left)}${text}${' '.repeat(right)}`;
53
- }
54
- if (align === 'right') {
55
- return `${' '.repeat(remaining)}${text}`;
56
- }
57
- return `${text}${' '.repeat(remaining)}`;
120
+
121
+ const visibleLength = ( value ) => stripAnsi( String( value ?? '' ) ).length;
122
+ const padText = ( value, width, align = 'left' ) => {
123
+
124
+ const text = ellipsize( value, width );
125
+ const current = visibleLength( text );
126
+ const remaining = width - current;
127
+ if ( remaining <= 0 ) return text;
128
+ if ( align === 'center' ) {
129
+
130
+ const left = Math.floor( remaining / 2 );
131
+ const right = remaining - left;
132
+ return `${' '.repeat( left )}${text}${' '.repeat( right )}`;
133
+
134
+ }
135
+
136
+ if ( align === 'right' ) {
137
+
138
+ return `${' '.repeat( remaining )}${text}`;
139
+
140
+ }
141
+
142
+ return `${text}${' '.repeat( remaining )}`;
143
+
58
144
  };
59
- const makeHeaderRow = (left, right = '', leftAlign = 'left', rightAlign = 'left') =>
60
- `│${padText(left, LEFT_WIDTH, leftAlign)}│${padText(right, RIGHT_WIDTH, rightAlign)}│`;
61
-
62
- const HEADER_COLOR = ESC(33);
63
- const CONTENT_COLOR = ESC(90); // bright black (grey)
64
- const reapplyColor = (value, color) => {
65
- const str = String(value ?? '');
66
- return `${color}${str.split(reset).join(`${reset}${color}`)}${reset}`;
145
+
146
+ const makeHeaderRow = ( left, right = '', leftAlign = 'left', rightAlign = 'left' ) =>
147
+ `│${padText( left, LEFT_WIDTH, leftAlign )}│${padText( right, RIGHT_WIDTH, rightAlign )}│`;
148
+
149
+ const HEADER_COLOR = ESC( 33 );
150
+ const CONTENT_COLOR = ESC( 90 ); // bright black (grey)
151
+ const reapplyColor = ( value, color ) => {
152
+
153
+ const str = String( value ?? '' );
154
+ return `${color}${str.split( reset ).join( `${reset}${color}` )}${reset}`;
155
+
67
156
  };
68
157
 
69
- const applyHeaderColor = (row, { keepContentYellow = false, tintContent = true } = {}) => {
70
- if (keepContentYellow) return reapplyColor(row, HEADER_COLOR);
71
- if (!row.startsWith('│') || !row.endsWith('│')) return reapplyColor(row, HEADER_COLOR);
72
- const match = row.match(/^│(.*)(.*)│$/);
73
- if (!match) return reapplyColor(row, HEADER_COLOR);
74
- const [, leftContent, rightContent] = match;
75
- const leftSegment = tintContent ? reapplyColor(leftContent, CONTENT_COLOR) : leftContent;
76
- const rightSegment = tintContent ? reapplyColor(rightContent, CONTENT_COLOR) : rightContent;
77
- return `${HEADER_COLOR}│${reset}${leftSegment}${HEADER_COLOR}│${reset}${rightSegment}${HEADER_COLOR}│${reset}`;
158
+ const applyHeaderColor = ( row, { keepContentYellow = false, tintContent = true } = {} ) => {
159
+
160
+ if ( keepContentYellow ) return reapplyColor( row, HEADER_COLOR );
161
+ if ( ! row.startsWith( '│' ) || ! row.endsWith( '│' ) ) return reapplyColor( row, HEADER_COLOR );
162
+ const match = row.match( /^│(.*)│(.*)│$/ );
163
+ if ( ! match ) return reapplyColor( row, HEADER_COLOR );
164
+ const [ , leftContent, rightContent ] = match;
165
+ const leftSegment = tintContent ? reapplyColor( leftContent, CONTENT_COLOR ) : leftContent;
166
+ const rightSegment = tintContent ? reapplyColor( rightContent, CONTENT_COLOR ) : rightContent;
167
+ return `${HEADER_COLOR}│${reset}${leftSegment}${HEADER_COLOR}│${reset}${rightSegment}${HEADER_COLOR}│${reset}`;
168
+
78
169
  };
79
170
 
80
- const formatRegistryShort = (value) => {
81
- if (!value) return '';
82
- try {
83
- const u = new URL(value);
84
- const pathname = (u.pathname || '').replace(/\/$/, '');
85
- return `${u.host}${pathname}`;
86
- } catch {
87
- return String(value);
88
- }
171
+ const formatRegistryShort = ( value ) => {
172
+
173
+ if ( ! value ) return '';
174
+ try {
175
+
176
+ const u = new URL( value );
177
+ const pathname = ( u.pathname || '' ).replace( /\/$/, '' );
178
+ return `${u.host}${pathname}`;
179
+
180
+ } catch {
181
+
182
+ return String( value );
183
+
184
+ }
185
+
89
186
  };
90
187
 
91
- const maskLicense = (s) => {
92
- if (!s) return '••••';
93
- const v = String(s);
94
- if (v.length <= 8) return '••••';
95
- return `${v.slice(0, 4)}••••${v.slice(-4)}`;
188
+ const maskLicense = ( s ) => {
189
+
190
+ if ( ! s ) return '••••';
191
+ const v = String( s );
192
+ if ( v.length <= 8 ) return '••••';
193
+ return `${v.slice( 0, 4 )}••••${v.slice( - 4 )}`;
194
+
96
195
  };
97
196
 
98
- const capitalize = (value) => {
99
- const str = String(value || '').trim();
100
- if (!str) return '';
101
- return str[0].toUpperCase() + str.slice(1);
197
+ const capitalize = ( value ) => {
198
+
199
+ const str = String( value || '' ).trim();
200
+ if ( ! str ) return '';
201
+ return str[ 0 ].toUpperCase() + str.slice( 1 );
202
+
102
203
  };
103
204
 
104
- const formatPlanLabel = (value) => {
105
- const str = String(value || '').trim();
106
- if (!str) return '';
107
- return str
108
- .split(/\s+/)
109
- .map((part) => (part ? capitalize(part.toLowerCase()) : ''))
110
- .join(' ')
111
- .trim();
205
+ const formatPlanLabel = ( value ) => {
206
+
207
+ const str = String( value || '' ).trim();
208
+ if ( ! str ) return '';
209
+ return str
210
+ .split( /\s+/ )
211
+ .map( ( part ) => ( part ? capitalize( part.toLowerCase() ) : '' ) )
212
+ .join( ' ' )
213
+ .trim();
214
+
112
215
  };
113
216
 
114
- const normalizePlan = (plan) => {
115
- const label = formatPlanLabel(plan);
116
- if (!label) return 'Developer Plan';
117
- return label.toLowerCase().includes('plan') ? label : `${label} Plan`;
217
+ const normalizePlan = ( plan ) => {
218
+
219
+ const label = formatPlanLabel( plan );
220
+ if ( ! label ) return 'Developer Plan';
221
+ return label.toLowerCase().includes( 'plan' ) ? label : `${label} Plan`;
222
+
118
223
  };
119
224
 
120
- const formatFirstName = (value) => {
121
- const name = String(value || '').trim();
122
- if (!name) return '';
123
- const first = name.split(/\s+/)[0];
124
- return capitalize(first.toLowerCase());
225
+ const formatFirstName = ( value ) => {
226
+
227
+ const name = String( value || '' ).trim();
228
+ if ( ! name ) return '';
229
+ const first = name.split( /\s+/ )[ 0 ];
230
+ return capitalize( first.toLowerCase() );
231
+
125
232
  };
126
233
 
127
234
  const getUserDisplayName = () => {
128
- const candidate =
235
+
236
+ const candidate =
129
237
  process.env.THREE_BLOCKS_USER_NAME ||
130
238
  process.env.GIT_AUTHOR_NAME ||
131
239
  process.env.USER ||
132
240
  process.env.LOGNAME;
133
- if (candidate) return formatFirstName(candidate);
134
- try {
135
- return formatFirstName(os.userInfo().username);
136
- } catch {
137
- return '';
138
- }
241
+ if ( candidate ) return formatFirstName( candidate );
242
+ try {
243
+
244
+ return formatFirstName( os.userInfo().username );
245
+
246
+ } catch {
247
+
248
+ return '';
249
+
250
+ }
251
+
139
252
  };
140
253
 
141
- const formatExpiryLabel = (iso) => {
142
- if (!iso) return 'Expires: —';
143
- const dt = new Date(iso);
144
- if (Number.isNaN(dt.getTime())) return `Expires: ${iso}`;
145
- return `Expires: ${dt.toISOString().replace('T', ' ').replace('Z', 'Z')}`;
254
+ const formatExpiryLabel = ( iso ) => {
255
+
256
+ if ( ! iso ) return 'Expires: —';
257
+ const dt = new Date( iso );
258
+ if ( Number.isNaN( dt.getTime() ) ) return `Expires: ${iso}`;
259
+ return `Expires: ${dt.toISOString().replace( 'T', ' ' ).replace( 'Z', 'Z' )}`;
260
+
146
261
  };
147
262
 
148
263
  const USER_DISPLAY_NAME = getUserDisplayName();
149
264
 
150
- const renderEnvHeader = ({
151
- scope,
152
- registry,
153
- expiresAt,
154
- tmpFile,
155
- licenseMasked,
156
- licenseId,
157
- channel,
158
- status,
159
- plan,
160
- teamName,
161
- teamId,
162
- repository,
163
- domain,
164
- region,
165
- userDisplayName,
166
- }) => {
167
- const title = `─── Three Blocks Login v${CLI_VERSION} `;
168
- const separatorRow = makeHeaderRow(repeatChar('─', LEFT_WIDTH), repeatChar('─', RIGHT_WIDTH));
169
- const channelDisplay = String(channel || '').toUpperCase() || 'STABLE';
170
- const registryShort = formatRegistryShort(registry);
171
-
172
- const displayName = userDisplayName || USER_DISPLAY_NAME;
173
- const welcomeLine = displayName ? `Welcome back ${displayName}!` : 'Welcome back!';
174
- const scopeLine = `Scope: ${scope}`;
175
-
176
- const planLabel = normalizePlan(plan);
177
- const teamLabel = teamName || teamId ? ` · Team: ${teamName || teamId}` : '';
178
- const subscriptionLine = `Plan: ${planLabel}${teamLabel}`;
179
- const channelLine = `Channel: ${channelDisplay}${region ? ` · Region: ${region}` : ''}`;
180
-
181
- const repositoryBase = repository ? `Repository: ${repository}` : 'Repository: —';
182
- const repositoryLine = registryShort ? `${repositoryBase} ${registryShort}` : repositoryBase;
183
- const registryLine = `Registry: ${registryShort || (registry || '—')}`;
184
-
185
- let domainValue = domain || '';
186
- if (!domainValue && registry) {
187
- try {
188
- domainValue = new URL(registry).host;
189
- } catch {}
190
- }
191
- const domainLineText = `Domain: ${domainValue || '—'}`;
192
- const regionLineText = `Region: ${region || '—'}`;
193
-
194
- const licenseLine = `License: ${licenseMasked}${licenseId ? ` · ${licenseId}` : ''}`;
195
- const expiresLine = formatExpiryLabel(expiresAt);
196
-
197
- const ascii = [
198
- 'THREE.JS',
199
- ' ______ __ ______ ______ __ __ ______ ',
200
- '/\\ == \\ /\\ \\ /\\ __ \\ /\\ ___\\ /\\ \\/ / /\\ ___\\ ',
201
- '\\ \\ __< \\ \\ \\____ \\ \\ \\/\\ \\ \\ \\ \\____ \\ \\ _"-. \\ \\___ \\ ',
202
- ' \\ \\_____\\ \\ \\_____\\ \\ \\_____\\ \\ \\_____\\ \\ \\_\\ \\_\\ \\/\\_____\\',
203
- ' \\/_____\/ \\/_____/ \\/_____/ \\/_____/ \\/_/ \/_/ \\/_____/'
204
- ];
205
- const statusMessage = status?.message || (status?.ok ? 'access granted' : status ? 'no active access' : 'pending');
206
- const statusText = status?.ok
207
- ? green(bold(`Authenticated ${statusMessage}`))
208
- : status
209
- ? red(bold(`Not authenticated — ${statusMessage}`))
210
- : dim(bold(`Status: ${statusMessage}`));
211
-
212
- const lines = [
213
- applyHeaderColor(`╭${title}${repeatChar('─', HEADER_WIDTH - 2 - title.length)}╮`, { keepContentYellow: true }),
214
- ...ascii.map((row) => applyHeaderColor(`│${padText(row, LEFT_WIDTH + RIGHT_WIDTH + 1, 'center')}│`, { keepContentYellow: true })),
215
- applyHeaderColor(separatorRow, { keepContentYellow: true }),
216
- applyHeaderColor(makeHeaderRow(welcomeLine, scopeLine, 'center', 'center')),
217
- applyHeaderColor(separatorRow, { keepContentYellow: true }),
218
- applyHeaderColor(makeHeaderRow(subscriptionLine, channelLine)),
219
- applyHeaderColor(makeHeaderRow(repositoryLine, registryLine)),
220
- applyHeaderColor(makeHeaderRow(domainLineText, regionLineText, 'left', 'center')),
221
- applyHeaderColor(makeHeaderRow(licenseLine, expiresLine)),
222
- applyHeaderColor(separatorRow, { keepContentYellow: true }),
223
- applyHeaderColor(makeHeaderRow(statusText, '', 'left', 'center'), { tintContent: false }),
224
- applyHeaderColor(`╰${repeatChar('─', HEADER_WIDTH - 2)}╯`, { keepContentYellow: true }),
225
- ];
226
- for (const row of lines) console.log(row);
265
+ const renderEnvHeader = ( {
266
+ scope,
267
+ registry,
268
+ expiresAt,
269
+ tmpFile,
270
+ licenseMasked,
271
+ licenseId,
272
+ channel,
273
+ status,
274
+ plan,
275
+ teamName,
276
+ teamId,
277
+ repository,
278
+ domain,
279
+ region,
280
+ userDisplayName,
281
+ } ) => {
282
+
283
+ const title = `─── Three Blocks Login v${CLI_VERSION} `;
284
+ const separatorRow = makeHeaderRow( repeatChar( '', LEFT_WIDTH ), repeatChar( '', RIGHT_WIDTH ) );
285
+ const channelDisplay = String( channel || '' ).toUpperCase() || 'STABLE';
286
+ const registryShort = formatRegistryShort( registry );
287
+
288
+ const displayName = userDisplayName || USER_DISPLAY_NAME;
289
+ const welcomeLine = displayName ? `Welcome back ${displayName}!` : 'Welcome back!';
290
+ const scopeLine = `Scope: ${scope}`;
291
+
292
+ const planLabel = normalizePlan( plan );
293
+ const teamLabel = teamName || teamId ? ` · Team: ${teamName || teamId}` : '';
294
+ const subscriptionLine = `Plan: ${planLabel}${teamLabel}`;
295
+ const channelLine = `Channel: ${channelDisplay}${region ? ` · Region: ${region}` : ''}`;
296
+
297
+ const repositoryBase = repository ? `Repository: ${repository}` : 'Repository: —';
298
+ const repositoryLine = registryShort ? `${repositoryBase} → ${registryShort}` : repositoryBase;
299
+ const registryLine = `Registry: ${registryShort || ( registry || '—' )}`;
300
+
301
+ let domainValue = domain || '';
302
+ if ( ! domainValue && registry ) {
303
+
304
+ try {
305
+
306
+ domainValue = new URL( registry ).host;
307
+
308
+ } catch {}
309
+
310
+ }
311
+
312
+ const domainLineText = `Domain: ${domainValue || '—'}`;
313
+ const regionLineText = `Region: ${region || ''}`;
314
+
315
+ const licenseLine = `License: ${licenseMasked}${licenseId ? ` · ${licenseId}` : ''}`;
316
+ const expiresLine = formatExpiryLabel( expiresAt );
317
+
318
+ const ascii = [
319
+ 'THREE.JS',
320
+ ' ______ __ ______ ______ __ __ ______ ',
321
+ '/\\ == \\ /\\ \\ /\\ __ \\ /\\ ___\\ /\\ \\/ / /\\ ___\\ ',
322
+ '\\ \\ __< \\ \\ \\____ \\ \\ \\/\\ \\ \\ \\ \\____ \\ \\ _"-. \\ \\___ \\ ',
323
+ ' \\ \\_____\\ \\ \\_____\\ \\ \\_____\\ \\ \\_____\\ \\ \\_\\ \\_\\ \\/\\_____\\',
324
+ ' \\/_____\/ \\/_____/ \\/_____/ \\/_____/ \\/_/ \/_/ \\/_____/'
325
+ ];
326
+ const statusMessage = status?.message || ( status?.ok ? 'access granted' : status ? 'no active access' : 'pending' );
327
+ const statusText = status?.ok
328
+ ? green( bold( `Authenticated ${statusMessage}` ) )
329
+ : status
330
+ ? red( bold( `Not authenticated — ${statusMessage}` ) )
331
+ : dim( bold( `Status: ${statusMessage}` ) );
332
+
333
+ const lines = [
334
+ applyHeaderColor( `╭${title}${repeatChar( '─', HEADER_WIDTH - 2 - title.length )}╮`, { keepContentYellow: true } ),
335
+ ...ascii.map( ( row ) => applyHeaderColor( `│${padText( row, LEFT_WIDTH + RIGHT_WIDTH + 1, 'center' )}│`, { keepContentYellow: true } ) ),
336
+ applyHeaderColor( separatorRow, { keepContentYellow: true } ),
337
+ applyHeaderColor( makeHeaderRow( welcomeLine, scopeLine, 'center', 'center' ) ),
338
+ applyHeaderColor( separatorRow, { keepContentYellow: true } ),
339
+ applyHeaderColor( makeHeaderRow( subscriptionLine, channelLine ) ),
340
+ applyHeaderColor( makeHeaderRow( repositoryLine, registryLine ) ),
341
+ applyHeaderColor( makeHeaderRow( domainLineText, regionLineText, 'left', 'center' ) ),
342
+ applyHeaderColor( makeHeaderRow( licenseLine, expiresLine ) ),
343
+ applyHeaderColor( separatorRow, { keepContentYellow: true } ),
344
+ applyHeaderColor( makeHeaderRow( statusText, '', 'left', 'center' ), { tintContent: false } ),
345
+ applyHeaderColor( `╰${repeatChar( '─', HEADER_WIDTH - 2 )}╯`, { keepContentYellow: true } ),
346
+ ];
347
+ for ( const row of lines ) console.log( row );
348
+
227
349
  };
228
350
 
229
- const args = parseArgs(process.argv.slice(2));
351
+ const args = parseArgs( process.argv.slice( 2 ) );
230
352
 
231
- const SCOPE = (args.scope || "@three-blocks").replace(/^\s+|\s+$/g, "");
353
+ DEBUG = DEBUG || !! args.debug || !! args.verbose;
354
+ if ( DEBUG ) logDebug( "Debug logging enabled." );
232
355
 
233
- const MODE = (args.mode || "env").toLowerCase(); // env | project | user
234
- const QUIET = !!args.quiet;
235
- const VERBOSE = !!args.verbose;
236
- let CHANNEL = String(args.channel || process.env.THREE_BLOCKS_CHANNEL || "stable").toLowerCase();
237
- if (!['stable','alpha','beta'].includes(CHANNEL)) CHANNEL = 'stable';
238
- const PRINT_SHELL = !!(args['print-shell'] || args.printShell || process.env.THREE_BLOCKS_LOGIN_PRINT_SHELL === '1');
239
- const NON_INTERACTIVE = !!(args['non-interactive'] || args.nonInteractive || process.env.CI === '1');
356
+ const SCOPE = ( args.scope || "@three-blocks" ).replace( /^\s+|\s+$/g, "" );
357
+
358
+ const MODE = ( args.mode || "env" ).toLowerCase(); // env | project | user
359
+ const QUIET = !! args.quiet;
360
+ const VERBOSE = !! args.verbose;
361
+ let CHANNEL = String( args.channel || process.env.THREE_BLOCKS_CHANNEL || "stable" ).toLowerCase();
362
+ if ( ! [ 'stable', 'alpha', 'beta' ].includes( CHANNEL ) ) CHANNEL = 'stable';
363
+ const PRINT_SHELL = !! ( args[ 'print-shell' ] || args.printShell || process.env.THREE_BLOCKS_LOGIN_PRINT_SHELL === '1' );
364
+ const NON_INTERACTIVE = !! ( args[ 'non-interactive' ] || args.nonInteractive || process.env.CI === '1' );
240
365
 
241
366
  // Load .env from current working directory (no deps)
242
- loadEnvFromDotfile(process.cwd());
367
+ loadEnvFromDotfile( process.cwd() );
243
368
 
244
369
  const BROKER_URL =
245
370
  args.endpoint ||
246
371
  process.env.THREE_BLOCKS_BROKER_URL ||
247
372
  "https://www.threejs-blocks.com/api/npm/token"; // your Astro broker endpoint
248
373
 
249
- console.log(BROKER_URL)
250
- const promptHidden = async (label) => {
251
- return await new Promise((resolve) => {
252
- const stdin = process.stdin;
253
- const stdout = process.stdout;
254
- let value = '';
255
- const cleanup = () => {
256
- stdin.removeListener('data', onData);
257
- if (stdin.isTTY) stdin.setRawMode(false);
258
- stdin.pause();
259
- };
260
- const onData = (chunk) => {
261
- const str = String(chunk);
262
- for (const ch of str) {
263
- if (ch === '\u0003') { cleanup(); process.exit(1); }
264
- if (ch === '\r' || ch === '\n' || ch === '\u0004') {
265
- stdout.write('\n');
266
- cleanup();
267
- return resolve(value.trim());
268
- }
269
- if (ch === '\b' || ch === '\u007f') {
270
- if (value.length) {
271
- value = value.slice(0, -1);
272
- readline.moveCursor(stdout, -1, 0);
273
- stdout.write(' ');
274
- readline.moveCursor(stdout, -1, 0);
275
- }
276
- continue;
277
- }
278
- if (ch === '\u001b') continue;
279
- value += ch;
280
- stdout.write('•');
281
- }
282
- };
283
- stdout.write(label);
284
- stdin.setEncoding('utf8');
285
- if (stdin.isTTY) stdin.setRawMode(true);
286
- stdin.resume();
287
- stdin.on('data', onData);
288
- });
374
+ console.log( BROKER_URL );
375
+ const promptHidden = async ( label ) => {
376
+
377
+ return await new Promise( ( resolve ) => {
378
+
379
+ const stdin = process.stdin;
380
+ const stdout = process.stdout;
381
+ let value = '';
382
+ const cleanup = () => {
383
+
384
+ stdin.removeListener( 'data', onData );
385
+ if ( stdin.isTTY ) stdin.setRawMode( false );
386
+ stdin.pause();
387
+
388
+ };
389
+
390
+ const onData = ( chunk ) => {
391
+
392
+ const str = String( chunk );
393
+ for ( const ch of str ) {
394
+
395
+ if ( ch === '\u0003' ) {
396
+
397
+ cleanup(); process.exit( 1 );
398
+
399
+ }
400
+
401
+ if ( ch === '\r' || ch === '\n' || ch === '\u0004' ) {
402
+
403
+ stdout.write( '\n' );
404
+ cleanup();
405
+ return resolve( value.trim() );
406
+
407
+ }
408
+
409
+ if ( ch === '\b' || ch === '\u007f' ) {
410
+
411
+ if ( value.length ) {
412
+
413
+ value = value.slice( 0, - 1 );
414
+ readline.moveCursor( stdout, - 1, 0 );
415
+ stdout.write( ' ' );
416
+ readline.moveCursor( stdout, - 1, 0 );
417
+
418
+ }
419
+
420
+ continue;
421
+
422
+ }
423
+
424
+ if ( ch === '\u001b' ) continue;
425
+ value += ch;
426
+ stdout.write( '•' );
427
+
428
+ }
429
+
430
+ };
431
+
432
+ stdout.write( label );
433
+ stdin.setEncoding( 'utf8' );
434
+ if ( stdin.isTTY ) stdin.setRawMode( true );
435
+ stdin.resume();
436
+ stdin.on( 'data', onData );
437
+
438
+ } );
439
+
289
440
  };
290
441
 
291
442
  // Pretty logger (respects --quiet except for errors)
292
443
  const log = {
293
- info: (msg) => { if (!QUIET) console.error(`${cyan("i")} ${msg}`); },
294
- ok: (msg) => { if (!QUIET) console.error(`${green("✔")} ${msg}`); },
295
- warn: (msg) => { if (!QUIET) console.error(`${yellow("")} ${msg}`); },
296
- error:(msg) => { console.error(`${red("✖")} ${msg}`); }
444
+ info: ( msg ) => {
445
+
446
+ if ( ! QUIET ) console.error( `${cyan( "i" )} ${msg}` );
447
+
448
+ },
449
+ ok: ( msg ) => {
450
+
451
+ if ( ! QUIET ) console.error( `${green( "✔" )} ${msg}` );
452
+
453
+ },
454
+ warn: ( msg ) => {
455
+
456
+ if ( ! QUIET ) console.error( `${yellow( "⚠" )} ${msg}` );
457
+
458
+ },
459
+ error: ( msg ) => {
460
+
461
+ console.error( `${red( "✖" )} ${msg}` );
462
+
463
+ }
297
464
  };
298
465
 
299
- (async () => {
300
- try {
301
- let LICENSE = args.license || process.env.THREE_BLOCKS_SECRET_KEY;
302
-
303
- if (!LICENSE || String(LICENSE).trim() === "") {
304
- if (NON_INTERACTIVE) {
305
- fail("Missing license key. Provide --license <key> or set THREE_BLOCKS_SECRET_KEY in your environment.");
306
- }
307
- console.log('');
308
- log.info(bold(yellow('Three Blocks Login')) + ' ' + dim(`[mode: ${MODE}]`));
309
- log.info(dim('Enter your license key to retrieve a scoped npm token.'));
310
- log.info(dim('Tip: paste it here; input is hidden. Press Enter to submit.'));
311
- LICENSE = await promptHidden(`${cyan('›')} License key ${dim('(tb_…)')}: `);
312
- if (!LICENSE) fail('License key is required to continue.');
313
- }
314
-
315
- // Sanitize + validate license early to avoid header ByteString errors
316
- const LICENSE_CLEAN = sanitizeLicense(LICENSE);
317
- if (!LICENSE_CLEAN) {
318
- fail(
319
- "Missing license key. Provide --license <key> or set THREE_BLOCKS_SECRET_KEY in your .env file."
320
- );
321
- }
322
- const invalidIdx = findFirstNonByteChar(LICENSE_CLEAN);
323
- if (invalidIdx !== -1) {
324
- if (VERBOSE) log.warn(`[debug] license contains non-ASCII/byte char at index ${invalidIdx}`);
325
- fail("License appears malformed. Please copy your tb_… key exactly without extra characters.");
326
- }
327
- if (!looksLikeLicense(LICENSE_CLEAN)) {
328
- if (VERBOSE) log.warn(`[debug] license failed format check: ${truncate(LICENSE_CLEAN, 16)}`);
329
- fail("License appears malformed. Please copy your tb_… key exactly.");
330
- }
331
-
332
- const tokenData = await fetchToken(BROKER_URL, LICENSE_CLEAN, CHANNEL);
333
- const {
334
- registry,
335
- token,
336
- expiresAt,
337
- status: rawStatus,
338
- plan,
339
- teamName,
340
- teamId,
341
- repository,
342
- domain,
343
- region,
344
- licenseId,
345
- } = tokenData;
346
- const authStatus = rawStatus ?? { ok: true, message: 'access granted' };
347
-
348
- if (!registry || !token) fail("Broker response missing registry/token.");
349
-
350
- const u = new URL(ensureTrailingSlash(registry));
351
- const hostPath = `${u.host}${u.pathname}`; // e.g. my-domain-.../npm/my-repo/
352
-
353
- const npmrcContent = [
354
- `${normalizeScope(SCOPE)}:registry=${u.href}`,
355
- `//${hostPath}:_authToken=${token}`
356
- ].join(os.EOL) + os.EOL;
357
-
358
- if (MODE === "env") {
359
- // Write to a temp .npmrc and export env so the *current shell* can reuse it
360
- const tmpFile = path.join(
361
- os.tmpdir(),
362
- `three-blocks-${Date.now()}-${Math.random().toString(36).slice(2)}.npmrc`
363
- );
364
- fs.writeFileSync(tmpFile, npmrcContent, { mode: 0o600 });
365
-
366
- // Print shell exports; caller should `eval "$(npx -y three-blocks-login --mode env)"`
367
- const maskedLicense = maskLicense(LICENSE_CLEAN);
368
- renderEnvHeader({
369
- scope: SCOPE,
370
- registry: u.href,
371
- expiresAt,
372
- tmpFile,
373
- licenseMasked: maskedLicense,
374
- licenseId,
375
- channel: CHANNEL,
376
- status: authStatus,
377
- plan,
378
- teamName,
379
- teamId,
380
- repository,
381
- domain,
382
- region,
383
- userDisplayName: USER_DISPLAY_NAME,
384
- });
385
- if (PRINT_SHELL) {
386
- const exportLines = [
387
- `export NPM_CONFIG_USERCONFIG=${quoteShell(tmpFile)}`,
388
- `export npm_config_userconfig=${quoteShell(tmpFile)}`,
389
- `export THREE_BLOCKS_CHANNEL=${quoteShell(CHANNEL)}`,
390
- ];
391
- const summaryPrint = [
392
- `cat <<'EOF'`,
393
- `${plainBanner} scoped login ready (${authStatus.ok ? 'ok' : 'error'})`,
394
- ` scope : ${SCOPE}`,
395
- ` registry : ${u.href}`,
396
- ` expires : ${expiresAt ?? 'unknown'}`,
397
- ` license : ${maskedLicense}`,
398
- ` npmrc : ${tmpFile}`,
399
- `EOF`,
400
- ];
401
- console.log([...exportLines, '', ...summaryPrint].join('\n'));
402
- } else {
403
- log.info(`${plainBanner} temp npmrc ready. Use --print-shell to emit export commands.`);
404
- }
405
- return;
406
- }
407
-
408
- if (MODE === "project") {
409
- // Write to local ./.npmrc (recommend users gitignore this)
410
- const out = path.resolve(process.cwd(), ".npmrc");
411
- fs.writeFileSync(out, npmrcContent, { mode: 0o600 });
412
- log.ok(`${banner} wrote ${bold(out)} ${dim(`(${SCOPE} → ${u.href}, expires ${expiresAt ?? "unknown"})`)}`);
413
- return;
414
- }
415
-
416
- if (MODE === "user") {
417
- // Update user's npm config (requires npm on PATH)
418
- await run("npm", ["config", "set", `${normalizeScope(SCOPE)}:registry`, u.href]);
419
- await run("npm", [
420
- "config",
421
- "set",
422
- `//${hostPath}:_authToken`,
423
- token
424
- ]);
425
- log.ok(`${banner} updated user npm config ${dim(`(${SCOPE} ${u.href}, expires ${expiresAt ?? "unknown"})`)}`);
426
- return;
427
- }
428
-
429
- fail(`Unknown --mode "${MODE}". Use env | project | user.`);
430
- } catch (e) {
431
- const msg = e?.message || String(e);
432
- if (VERBOSE) {
433
- log.error(`[debug] ${msg}`);
434
- return process.exit(1);
435
- }
436
- if (/ByteString/i.test(msg) || /character at index/i.test(msg)) {
437
- return fail("License appears malformed. Please copy your tb_… key exactly and retry.");
438
- }
439
- return fail("Authentication failed. Please try again. Use --verbose for details.");
440
- }
441
- })();
442
-
443
- function normalizeScope(scope) {
444
- return scope.startsWith("@") ? scope : `@${scope}`;
466
+ ( async () => {
467
+
468
+ try {
469
+
470
+ let LICENSE = args.license || process.env.THREE_BLOCKS_SECRET_KEY;
471
+
472
+ if ( ! LICENSE || String( LICENSE ).trim() === "" ) {
473
+
474
+ if ( NON_INTERACTIVE ) {
475
+
476
+ fail(
477
+ "Missing license key. Provide --license <key> or set THREE_BLOCKS_SECRET_KEY in your environment.",
478
+ { suggestion: "Pass --license tb_… or export THREE_BLOCKS_SECRET_KEY before running the CLI." }
479
+ );
480
+
481
+ }
482
+
483
+ console.log( '' );
484
+ log.info( bold( yellow( 'Three Blocks Login' ) ) + ' ' + dim( `[mode: ${MODE}]` ) );
485
+ log.info( dim( 'Enter your license key to retrieve a scoped npm token.' ) );
486
+ log.info( dim( 'Tip: paste it here; input is hidden. Press Enter to submit.' ) );
487
+ LICENSE = await promptHidden( `${cyan( '›' )} License key ${dim( '(tb_…)' )}: ` );
488
+ if ( ! LICENSE ) {
489
+
490
+ fail(
491
+ "License key is required to continue.",
492
+ { suggestion: "Paste your tb_… key or rerun with --license." }
493
+ );
494
+
495
+ }
496
+
497
+ }
498
+
499
+ // Sanitize + validate license early to avoid header ByteString errors
500
+ const LICENSE_CLEAN = sanitizeLicense( LICENSE );
501
+ if ( ! LICENSE_CLEAN ) {
502
+
503
+ fail(
504
+ "Missing license key. Provide --license <key> or set THREE_BLOCKS_SECRET_KEY in your .env file.",
505
+ { suggestion: "Set THREE_BLOCKS_SECRET_KEY=tb_… in your environment or pass --license tb_…" }
506
+ );
507
+
508
+ }
509
+
510
+ const invalidIdx = findFirstNonByteChar( LICENSE_CLEAN );
511
+ if ( invalidIdx !== - 1 ) {
512
+
513
+ if ( VERBOSE ) log.warn( `[debug] license contains non-ASCII/byte char at index ${invalidIdx}` );
514
+ fail(
515
+ "License appears malformed. Please copy your tb_… key exactly without extra characters.",
516
+ { suggestion: "Remove spaces or smart quotes around the tb_… key and retry." }
517
+ );
518
+
519
+ }
520
+
521
+ if ( ! looksLikeLicense( LICENSE_CLEAN ) ) {
522
+
523
+ if ( VERBOSE ) log.warn( `[debug] license failed format check: ${truncate( LICENSE_CLEAN, 16 )}` );
524
+ fail(
525
+ "License appears malformed. Please copy your tb_… key exactly.",
526
+ { suggestion: "Double-check the license string and avoid truncating it." }
527
+ );
528
+
529
+ }
530
+
531
+ const tokenData = await fetchToken( BROKER_URL, LICENSE_CLEAN, CHANNEL );
532
+ const {
533
+ registry,
534
+ token,
535
+ expiresAt,
536
+ status: rawStatus,
537
+ plan,
538
+ teamName,
539
+ teamId,
540
+ repository,
541
+ domain,
542
+ region,
543
+ licenseId,
544
+ } = tokenData;
545
+ const authStatus = rawStatus ?? { ok: true, message: 'access granted' };
546
+
547
+ if ( ! registry || ! token ) {
548
+
549
+ fail(
550
+ "Broker response missing registry/token.",
551
+ { suggestion: "Retry the login or contact support; the broker response was incomplete." }
552
+ );
553
+
554
+ }
555
+
556
+ const u = new URL( ensureTrailingSlash( registry ) );
557
+ const hostPath = `${u.host}${u.pathname}`; // e.g. my-domain-.../npm/my-repo/
558
+
559
+ const npmrcContent = [
560
+ `${normalizeScope( SCOPE )}:registry=${u.href}`,
561
+ `//${hostPath}:_authToken=${token}`
562
+ ].join( os.EOL ) + os.EOL;
563
+
564
+ if ( MODE === "env" ) {
565
+
566
+ // Write to a temp .npmrc and export env so the *current shell* can reuse it
567
+ const tmpFile = path.join(
568
+ os.tmpdir(),
569
+ `three-blocks-${Date.now()}-${Math.random().toString( 36 ).slice( 2 )}.npmrc`
570
+ );
571
+ fs.writeFileSync( tmpFile, npmrcContent, { mode: 0o600 } );
572
+
573
+ // Print shell exports; caller should `eval "$(npx -y three-blocks-login --mode env)"`
574
+ const maskedLicense = maskLicense( LICENSE_CLEAN );
575
+ renderEnvHeader( {
576
+ scope: SCOPE,
577
+ registry: u.href,
578
+ expiresAt,
579
+ tmpFile,
580
+ licenseMasked: maskedLicense,
581
+ licenseId,
582
+ channel: CHANNEL,
583
+ status: authStatus,
584
+ plan,
585
+ teamName,
586
+ teamId,
587
+ repository,
588
+ domain,
589
+ region,
590
+ userDisplayName: USER_DISPLAY_NAME,
591
+ } );
592
+ if ( PRINT_SHELL ) {
593
+
594
+ const exportLines = [
595
+ `export NPM_CONFIG_USERCONFIG=${quoteShell( tmpFile )}`,
596
+ `export npm_config_userconfig=${quoteShell( tmpFile )}`,
597
+ `export THREE_BLOCKS_CHANNEL=${quoteShell( CHANNEL )}`,
598
+ ];
599
+ const summaryPrint = [
600
+ `cat <<'EOF'`,
601
+ `${plainBanner} scoped login ready (${authStatus.ok ? 'ok' : 'error'})`,
602
+ ` scope : ${SCOPE}`,
603
+ ` registry : ${u.href}`,
604
+ ` expires : ${expiresAt ?? 'unknown'}`,
605
+ ` license : ${maskedLicense}`,
606
+ ` npmrc : ${tmpFile}`,
607
+ `EOF`,
608
+ ];
609
+ console.log( [ ...exportLines, '', ...summaryPrint ].join( '\n' ) );
610
+
611
+ } else {
612
+
613
+ log.info( `${plainBanner} temp npmrc ready. Use --print-shell to emit export commands.` );
614
+
615
+ }
616
+
617
+ return;
618
+
619
+ }
620
+
621
+ if ( MODE === "project" ) {
622
+
623
+ // Write to local ./.npmrc (recommend users gitignore this)
624
+ const out = path.resolve( process.cwd(), ".npmrc" );
625
+ fs.writeFileSync( out, npmrcContent, { mode: 0o600 } );
626
+ log.ok( `${banner} wrote ${bold( out )} ${dim( `(${SCOPE} → ${u.href}, expires ${expiresAt ?? "unknown"})` )}` );
627
+ return;
628
+
629
+ }
630
+
631
+ if ( MODE === "user" ) {
632
+
633
+ // Update user's npm config (requires npm on PATH)
634
+ await run( "npm", [ "config", "set", `${normalizeScope( SCOPE )}:registry`, u.href ] );
635
+ await run( "npm", [
636
+ "config",
637
+ "set",
638
+ `//${hostPath}:_authToken`,
639
+ token
640
+ ] );
641
+ log.ok( `${banner} updated user npm config ${dim( `(${SCOPE} → ${u.href}, expires ${expiresAt ?? "unknown"})` )}` );
642
+ return;
643
+
644
+ }
645
+
646
+ fail(
647
+ `Unknown --mode "${MODE}". Use env | project | user.`,
648
+ { suggestion: "Pass --mode env, --mode project, or --mode user." }
649
+ );
650
+
651
+ } catch ( error ) {
652
+
653
+ let err = error;
654
+ if ( err instanceof CliError === false ) {
655
+
656
+ const message = error?.message || String( error );
657
+ if ( /ByteString/i.test( message ) || /character at index/i.test( message ) ) {
658
+
659
+ err = new CliError(
660
+ "License appears malformed. Please copy your tb_… key exactly and retry.",
661
+ { cause: error }
662
+ );
663
+
664
+ } else {
665
+
666
+ err = new CliError(
667
+ "Authentication failed. Please try again. Use --verbose or --debug for more details.",
668
+ { cause: error }
669
+ );
670
+
671
+ }
672
+
673
+ }
674
+
675
+ log.error( `${banner} ${err.message}` );
676
+
677
+ const detail = ( err.stderr || err.stdout || "" ).trim();
678
+ if ( detail ) {
679
+
680
+ for ( const line of detail.split( /\r?\n/ ) ) {
681
+
682
+ console.error( dim( ` ${line}` ) );
683
+
684
+ }
685
+
686
+ }
687
+
688
+ if ( err.suggestion ) log.info( dim( `Hint: ${err.suggestion}` ) );
689
+ if ( DEBUG && err.cause && err.cause !== err && err.cause?.stack ) {
690
+
691
+ console.error( dim( err.cause.stack ) );
692
+
693
+ } else if ( DEBUG && err.stack ) {
694
+
695
+ console.error( dim( err.stack ) );
696
+
697
+ }
698
+
699
+ process.exit( err.exitCode ?? 1 );
700
+
701
+ }
702
+
703
+ } )();
704
+
705
+ function normalizeScope( scope ) {
706
+
707
+ return scope.startsWith( "@" ) ? scope : `@${scope}`;
708
+
445
709
  }
446
710
 
447
- function ensureTrailingSlash(url) {
448
- return url.endsWith("/") ? url : url + "/";
711
+ function ensureTrailingSlash( url ) {
712
+
713
+ return url.endsWith( "/" ) ? url : url + "/";
714
+
449
715
  }
450
716
 
451
- function loadEnvFromDotfile(dir) {
452
- try {
453
- const files = [path.join(dir, ".env.local"), path.join(dir, ".env")];
454
- for (const file of files) {
455
- if (!fs.existsSync(file)) continue;
456
- const txt = fs.readFileSync(file, "utf8");
457
- for (const raw of txt.split(/\r?\n/)) {
458
- const line = raw.trim();
459
- if (!line || line.startsWith("#")) continue;
460
- const eq = line.indexOf("=");
461
- if (eq === -1) continue;
462
- const key = line.slice(0, eq).trim();
463
- let val = line.slice(eq + 1).trim();
464
- if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
465
- val = val.slice(1, -1);
466
- }
467
- if (process.env[key] === undefined) process.env[key] = val;
468
- }
469
- }
470
- } catch {}
717
+ function loadEnvFromDotfile( dir ) {
718
+
719
+ try {
720
+
721
+ const files = [ path.join( dir, ".env.local" ), path.join( dir, ".env" ) ];
722
+ for ( const file of files ) {
723
+
724
+ if ( ! fs.existsSync( file ) ) continue;
725
+ const txt = fs.readFileSync( file, "utf8" );
726
+ for ( const raw of txt.split( /\r?\n/ ) ) {
727
+
728
+ const line = raw.trim();
729
+ if ( ! line || line.startsWith( "#" ) ) continue;
730
+ const eq = line.indexOf( "=" );
731
+ if ( eq === - 1 ) continue;
732
+ const key = line.slice( 0, eq ).trim();
733
+ let val = line.slice( eq + 1 ).trim();
734
+ if ( ( val.startsWith( '"' ) && val.endsWith( '"' ) ) || ( val.startsWith( "'" ) && val.endsWith( "'" ) ) ) {
735
+
736
+ val = val.slice( 1, - 1 );
737
+
738
+ }
739
+
740
+ if ( process.env[ key ] === undefined ) process.env[ key ] = val;
741
+
742
+ }
743
+
744
+ }
745
+
746
+ } catch {}
747
+
471
748
  }
472
749
 
473
- async function fetchToken(endpoint, license, channel) {
474
- let url = endpoint;
475
- try {
476
- const u = new URL(endpoint);
477
- if (!u.searchParams.get('channel')) u.searchParams.set('channel', channel);
478
- url = u.toString();
479
- } catch {}
480
- const res = await fetch(url, {
481
- method: "GET",
482
- headers: {
483
- "authorization": `Bearer ${license}`,
484
- "accept": "application/json",
485
- "x-three-blocks-channel": channel
486
- }
487
- });
488
- if (!res.ok) {
489
- let body = "";
490
- try { body = await res.text(); } catch {}
491
- if (VERBOSE) {
492
- log.warn(`[debug] broker response: status=${res.status} body=${truncate(body, 300)}`);
493
- }
494
- let msg = "Request failed. Please try again.";
495
- if (res.status === 401 || res.status === 403) {
496
- msg = "Authentication failed. Verify your license or access rights.";
497
- } else if (res.status === 404) {
498
- msg = "Service not found or endpoint misconfigured.";
499
- } else if (res.status === 429) {
500
- msg = "Too many requests. Please retry later.";
501
- } else if (res.status >= 500) {
502
- msg = "Upstream service error. Please retry later.";
503
- }
504
- throw new Error(msg);
505
- }
506
- const json = await res.json();
507
- // expected: { registry, token, expiresAt? }
508
- return json;
750
+ async function fetchToken( endpoint, license, channel ) {
751
+
752
+ let url = endpoint;
753
+ try {
754
+
755
+ const u = new URL( endpoint );
756
+ if ( ! u.searchParams.get( 'channel' ) ) u.searchParams.set( 'channel', channel );
757
+ url = u.toString();
758
+
759
+ } catch {}
760
+
761
+ logDebug( `GET ${url}` );
762
+ let res;
763
+ try {
764
+
765
+ res = await fetch( url, {
766
+ method: "GET",
767
+ headers: {
768
+ "authorization": `Bearer ${license}`,
769
+ "accept": "application/json",
770
+ "x-three-blocks-channel": channel
771
+ }
772
+ } );
773
+
774
+ } catch ( networkError ) {
775
+
776
+ throw new CliError(
777
+ "Could not reach the token broker endpoint.",
778
+ {
779
+ suggestion: "Check your network connection or override the endpoint with --endpoint.",
780
+ cause: networkError
781
+ }
782
+ );
783
+
784
+ }
785
+
786
+ let responseText = "";
787
+ try {
788
+
789
+ responseText = await res.text();
790
+
791
+ } catch ( readError ) {
792
+
793
+ logDebug( `Failed to read broker response body: ${readError?.message || readError}` );
794
+
795
+ }
796
+
797
+ if ( DEBUG ) {
798
+
799
+ logDebug( `Broker response status=${res.status} body=${truncate( responseText, 300 )}` );
800
+
801
+ }
802
+
803
+ if ( ! res.ok ) {
804
+
805
+ let suggestion = "Request failed. Please retry.";
806
+ if ( res.status === 401 || res.status === 403 ) {
807
+
808
+ suggestion = "Authentication failed. Verify your license or access rights.";
809
+
810
+ } else if ( res.status === 404 ) {
811
+
812
+ suggestion = "Service not found or endpoint misconfigured.";
813
+
814
+ } else if ( res.status === 429 ) {
815
+
816
+ suggestion = "Too many requests. Please retry later.";
817
+
818
+ } else if ( res.status >= 500 ) {
819
+
820
+ suggestion = "Upstream service error. Please retry later.";
821
+
822
+ }
823
+
824
+ throw new CliError(
825
+ `Token broker request failed (HTTP ${res.status}).`,
826
+ {
827
+ exitCode: 1,
828
+ stdout: responseText,
829
+ suggestion,
830
+ }
831
+ );
832
+
833
+ }
834
+
835
+ if ( ! responseText ) {
836
+
837
+ throw new CliError(
838
+ "Token broker returned an empty response.",
839
+ {
840
+ suggestion: "Retry in a few moments or contact support if the issue persists."
841
+ }
842
+ );
843
+
844
+ }
845
+
846
+ try {
847
+
848
+ const json = JSON.parse( responseText );
849
+ return json;
850
+
851
+ } catch ( parseError ) {
852
+
853
+ throw new CliError(
854
+ "Token broker returned invalid JSON.",
855
+ {
856
+ stdout: responseText,
857
+ suggestion: "Retry in a few moments or contact support if the issue persists.",
858
+ cause: parseError,
859
+ }
860
+ );
861
+
862
+ }
863
+
509
864
  }
510
865
 
511
- function truncate(s, n) {
512
- if (!s) return s;
513
- return s.length > n ? s.slice(0, n) + "…" : s;
866
+ function truncate( s, n ) {
867
+
868
+ if ( ! s ) return s;
869
+ return s.length > n ? s.slice( 0, n ) + "…" : s;
870
+
514
871
  }
515
872
 
516
- function sanitizeLicense(x) {
517
- if (!x) return "";
518
- let s = String(x);
519
- // Strip surrounding quotes
520
- s = s.trim().replace(/^['"]+|['"]+$/g, "");
521
- // Remove common pasted/hidden chars: bullet, zero-width, NBSP
522
- s = s
523
- .replace(/[\u2022\u2023\u25E6\u2043\u2219]/g, "") // bullets
524
- .replace(/[\u200B-\u200D\uFEFF]/g, "") // zero-width
525
- .replace(/\u00A0/g, " ") // NBSP -> space
526
- .replace(/\s+/g, ""); // remove whitespace
527
- // Normalize fancy quotes to plain
528
- s = s.replace(/[\u2018\u2019\u201C\u201D]/g, "");
529
- return s;
873
+ function sanitizeLicense( x ) {
874
+
875
+ if ( ! x ) return "";
876
+ let s = String( x );
877
+ // Strip surrounding quotes
878
+ s = s.trim().replace( /^['"]+|['"]+$/g, "" );
879
+ // Remove common pasted/hidden chars: bullet, zero-width, NBSP
880
+ s = s
881
+ .replace( /[\u2022\u2023\u25E6\u2043\u2219]/g, "" ) // bullets
882
+ .replace( /[\u200B-\u200D\uFEFF]/g, "" ) // zero-width
883
+ .replace( /\u00A0/g, " " ) // NBSP -> space
884
+ .replace( /\s+/g, "" ); // remove whitespace
885
+ // Normalize fancy quotes to plain
886
+ s = s.replace( /[\u2018\u2019\u201C\u201D]/g, "" );
887
+ return s;
888
+
530
889
  }
531
890
 
532
- function findFirstNonByteChar(s) {
533
- if (!s) return -1;
534
- for (let i = 0; i < s.length; i++) {
535
- const code = s.charCodeAt(i);
536
- if (code > 255) return i;
537
- }
538
- return -1;
891
+ function findFirstNonByteChar( s ) {
892
+
893
+ if ( ! s ) return - 1;
894
+ for ( let i = 0; i < s.length; i ++ ) {
895
+
896
+ const code = s.charCodeAt( i );
897
+ if ( code > 255 ) return i;
898
+
899
+ }
900
+
901
+ return - 1;
902
+
539
903
  }
540
904
 
541
- function looksLikeLicense(s) {
542
- if (!s) return false;
543
- // tb_ followed by base64url-ish payload
544
- return /^tb_[A-Za-z0-9_-]{10,}$/.test(s);
905
+ function looksLikeLicense( s ) {
906
+
907
+ if ( ! s ) return false;
908
+ // tb_ followed by base64url-ish payload
909
+ return /^tb_[A-Za-z0-9_-]{10,}$/.test( s );
910
+
545
911
  }
546
912
 
547
- async function run(cmd, args) {
548
- const { spawn } = await import("node:child_process");
549
- return new Promise((resolve, reject) => {
550
- const p = spawn(cmd, args, { stdio: "inherit", shell: true });
551
- p.on("close", (code) => {
552
- if (code === 0) resolve(undefined);
553
- else reject(new Error(`${cmd} ${args.join(" ")} exited with ${code}`));
554
- });
555
- });
913
+ async function run( cmd, args ) {
914
+
915
+ const { spawn } = await import( "node:child_process" );
916
+ const spawnArgs = Array.isArray( args ) ? args : [];
917
+ logDebug( `exec ${formatCommand( cmd, spawnArgs )}` );
918
+ return new Promise( ( resolve, reject ) => {
919
+
920
+ const child = spawn( cmd, spawnArgs, {
921
+ stdio: "inherit",
922
+ shell: process.platform === "win32",
923
+ env: process.env,
924
+ } );
925
+ child.on( "error", ( error ) => {
926
+
927
+ reject(
928
+ new CliError(
929
+ `Failed to start ${cmd}: ${error.message}`,
930
+ { command: cmd, args: spawnArgs, cause: error }
931
+ )
932
+ );
933
+
934
+ } );
935
+ child.on( "close", ( code ) => {
936
+
937
+ if ( code === 0 ) resolve( undefined );
938
+ else {
939
+
940
+ reject(
941
+ new CliError(
942
+ `Command failed (${formatCommand( cmd, spawnArgs )}) [exit ${code}]`,
943
+ { exitCode: code ?? 1, command: cmd, args: spawnArgs }
944
+ )
945
+ );
946
+
947
+ }
948
+
949
+ } );
950
+
951
+ } );
952
+
556
953
  }
557
954
 
558
- function parseArgs(argv) {
559
- const out = {};
560
- for (let i = 0; i < argv.length; i++) {
561
- const a = argv[i];
562
- if (!a.startsWith("-")) continue;
563
- const key = a.replace(/^-+/, "");
564
- const next = argv[i + 1];
565
- if (["--quiet", "-q"].includes(a)) out.quiet = true;
566
- else if (["--verbose", "-v"].includes(a)) out.verbose = true;
567
- else if (next && !next.startsWith("-")) {
568
- out[key] = next;
569
- i++;
570
- } else {
571
- out[key] = true;
572
- }
573
- }
574
- return out;
955
+ function parseArgs( argv ) {
956
+
957
+ const out = {};
958
+ for ( let i = 0; i < argv.length; i ++ ) {
959
+
960
+ const a = argv[ i ];
961
+ if ( ! a.startsWith( "-" ) ) continue;
962
+ const key = a.replace( /^-+/, "" );
963
+ const next = argv[ i + 1 ];
964
+ if ( [ "--quiet", "-q" ].includes( a ) ) out.quiet = true;
965
+ else if ( [ "--verbose", "-v" ].includes( a ) ) out.verbose = true;
966
+ else if ( next && ! next.startsWith( "-" ) ) {
967
+
968
+ out[ key ] = next;
969
+ i ++;
970
+
971
+ } else {
972
+
973
+ out[ key ] = true;
974
+
975
+ }
976
+
977
+ }
978
+
979
+ return out;
980
+
575
981
  }
576
982
 
577
- function fail(msg) {
578
- console.error(`${red("✖")} ${banner} ${msg}`);
579
- process.exit(1);
983
+ function fail( msg, options = {} ) {
984
+
985
+ throw new CliError( msg, options );
986
+
580
987
  }