three-blocks-login 0.1.2 → 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.
- package/bin/login.js +902 -494
- package/package.json +1 -1
package/bin/login.js
CHANGED
|
@@ -8,572 +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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}) => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
353
|
+
DEBUG = DEBUG || !! args.debug || !! args.verbose;
|
|
354
|
+
if ( DEBUG ) logDebug( "Debug logging enabled." );
|
|
232
355
|
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
"
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
372
|
+
"https://www.threejs-blocks.com/api/npm/token"; // your Astro broker endpoint
|
|
373
|
+
|
|
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
|
+
|
|
288
440
|
};
|
|
289
441
|
|
|
290
442
|
// Pretty logger (respects --quiet except for errors)
|
|
291
443
|
const log = {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|
|
296
464
|
};
|
|
297
465
|
|
|
298
|
-
(async () => {
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
+
|
|
444
709
|
}
|
|
445
710
|
|
|
446
|
-
function ensureTrailingSlash(url) {
|
|
447
|
-
|
|
711
|
+
function ensureTrailingSlash( url ) {
|
|
712
|
+
|
|
713
|
+
return url.endsWith( "/" ) ? url : url + "/";
|
|
714
|
+
|
|
448
715
|
}
|
|
449
716
|
|
|
450
|
-
function loadEnvFromDotfile(dir) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
+
|
|
470
748
|
}
|
|
471
749
|
|
|
472
|
-
async function fetchToken(endpoint, license, channel) {
|
|
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
|
-
|
|
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
|
+
|
|
508
864
|
}
|
|
509
865
|
|
|
510
|
-
function truncate(s, n) {
|
|
511
|
-
|
|
512
|
-
|
|
866
|
+
function truncate( s, n ) {
|
|
867
|
+
|
|
868
|
+
if ( ! s ) return s;
|
|
869
|
+
return s.length > n ? s.slice( 0, n ) + "…" : s;
|
|
870
|
+
|
|
513
871
|
}
|
|
514
872
|
|
|
515
|
-
function sanitizeLicense(x) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
+
|
|
529
889
|
}
|
|
530
890
|
|
|
531
|
-
function findFirstNonByteChar(s) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
|
|
538
903
|
}
|
|
539
904
|
|
|
540
|
-
function looksLikeLicense(s) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
+
|
|
544
911
|
}
|
|
545
912
|
|
|
546
|
-
async function run(cmd, args) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
+
|
|
555
953
|
}
|
|
556
954
|
|
|
557
|
-
function parseArgs(argv) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
+
|
|
574
981
|
}
|
|
575
982
|
|
|
576
|
-
function fail(msg) {
|
|
577
|
-
|
|
578
|
-
|
|
983
|
+
function fail( msg, options = {} ) {
|
|
984
|
+
|
|
985
|
+
throw new CliError( msg, options );
|
|
986
|
+
|
|
579
987
|
}
|