three-blocks-login 0.1.1 → 0.1.3
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 +333 -39
- package/package.json +1 -1
package/bin/login.js
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
|
+
import readline from "node:readline";
|
|
8
9
|
import { createRequire } from "node:module";
|
|
9
10
|
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
const pkg = require("../package.json");
|
|
13
|
+
const CLI_VERSION = pkg?.version ? String(pkg.version) : "?.?.?";
|
|
12
14
|
|
|
13
15
|
// Simple ANSI color helpers (no deps)
|
|
14
16
|
const ESC = (n) => `\u001b[${n}m`;
|
|
@@ -20,7 +22,209 @@ const green = (s) => ESC(32) + s + reset;
|
|
|
20
22
|
const yellow = (s) => ESC(33) + s + reset;
|
|
21
23
|
const cyan = (s) => ESC(36) + s + reset;
|
|
22
24
|
const plainBanner = `[three-blocks-login@${pkg.version}]`;
|
|
23
|
-
const banner =
|
|
25
|
+
const banner = cyan(plainBanner);
|
|
26
|
+
|
|
27
|
+
const quoteShell = (value) => JSON.stringify(String(value ?? ''));
|
|
28
|
+
|
|
29
|
+
const HEADER_WIDTH = 96;
|
|
30
|
+
const LEFT_WIDTH = 60;
|
|
31
|
+
const RIGHT_WIDTH = HEADER_WIDTH - LEFT_WIDTH - 3;
|
|
32
|
+
|
|
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;
|
|
42
|
+
};
|
|
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)}`;
|
|
58
|
+
};
|
|
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}`;
|
|
67
|
+
};
|
|
68
|
+
|
|
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}`;
|
|
78
|
+
};
|
|
79
|
+
|
|
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
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
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)}`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const capitalize = (value) => {
|
|
99
|
+
const str = String(value || '').trim();
|
|
100
|
+
if (!str) return '';
|
|
101
|
+
return str[0].toUpperCase() + str.slice(1);
|
|
102
|
+
};
|
|
103
|
+
|
|
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();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const normalizePlan = (plan) => {
|
|
115
|
+
const label = formatPlanLabel(plan);
|
|
116
|
+
if (!label) return 'Developer Plan';
|
|
117
|
+
return label.toLowerCase().includes('plan') ? label : `${label} Plan`;
|
|
118
|
+
};
|
|
119
|
+
|
|
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());
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const getUserDisplayName = () => {
|
|
128
|
+
const candidate =
|
|
129
|
+
process.env.THREE_BLOCKS_USER_NAME ||
|
|
130
|
+
process.env.GIT_AUTHOR_NAME ||
|
|
131
|
+
process.env.USER ||
|
|
132
|
+
process.env.LOGNAME;
|
|
133
|
+
if (candidate) return formatFirstName(candidate);
|
|
134
|
+
try {
|
|
135
|
+
return formatFirstName(os.userInfo().username);
|
|
136
|
+
} catch {
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
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')}`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const USER_DISPLAY_NAME = getUserDisplayName();
|
|
149
|
+
|
|
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);
|
|
227
|
+
};
|
|
24
228
|
|
|
25
229
|
const args = parseArgs(process.argv.slice(2));
|
|
26
230
|
|
|
@@ -31,6 +235,8 @@ const QUIET = !!args.quiet;
|
|
|
31
235
|
const VERBOSE = !!args.verbose;
|
|
32
236
|
let CHANNEL = String(args.channel || process.env.THREE_BLOCKS_CHANNEL || "stable").toLowerCase();
|
|
33
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');
|
|
34
240
|
|
|
35
241
|
// Load .env from current working directory (no deps)
|
|
36
242
|
loadEnvFromDotfile(process.cwd());
|
|
@@ -38,34 +244,49 @@ loadEnvFromDotfile(process.cwd());
|
|
|
38
244
|
const BROKER_URL =
|
|
39
245
|
args.endpoint ||
|
|
40
246
|
process.env.THREE_BLOCKS_BROKER_URL ||
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
247
|
+
"https://www.threejs-blocks.com/api/npm/token"; // your Astro broker endpoint
|
|
248
|
+
|
|
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
|
+
});
|
|
289
|
+
};
|
|
69
290
|
|
|
70
291
|
// Pretty logger (respects --quiet except for errors)
|
|
71
292
|
const log = {
|
|
@@ -77,8 +298,52 @@ const log = {
|
|
|
77
298
|
|
|
78
299
|
(async () => {
|
|
79
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
|
+
|
|
80
332
|
const tokenData = await fetchToken(BROKER_URL, LICENSE_CLEAN, CHANNEL);
|
|
81
|
-
const {
|
|
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' };
|
|
82
347
|
|
|
83
348
|
if (!registry || !token) fail("Broker response missing registry/token.");
|
|
84
349
|
|
|
@@ -99,15 +364,44 @@ const log = {
|
|
|
99
364
|
fs.writeFileSync(tmpFile, npmrcContent, { mode: 0o600 });
|
|
100
365
|
|
|
101
366
|
// Print shell exports; caller should `eval "$(npx -y three-blocks-login --mode env)"`
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
}
|
|
111
405
|
return;
|
|
112
406
|
}
|
|
113
407
|
|