create-three-blocks-starter 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +326 -25
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -10,21 +10,51 @@ import path from 'node:path';
|
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
12
|
import readline from 'node:readline';
|
|
13
|
-
import { bold, cyan, dim,
|
|
13
|
+
import { bold, cyan, dim, green, red, yellow } from 'kolorist';
|
|
14
|
+
|
|
15
|
+
const ESC = (n) => `\u001b[${n}m`;
|
|
16
|
+
const reset = ESC(0);
|
|
14
17
|
|
|
15
18
|
const STARTER_PKG = '@three-blocks/starter'; // private starter (lives in CodeArtifact)
|
|
16
19
|
const LOGIN_CLI = 'three-blocks-login'; // public login CLI
|
|
20
|
+
const LOGIN_SPEC = `${LOGIN_CLI}@latest`; // force-fresh npx install
|
|
17
21
|
const SCOPE = '@three-blocks';
|
|
18
22
|
|
|
19
|
-
const
|
|
23
|
+
const BLOCKED_NPM_ENV_KEYS = new Set([
|
|
24
|
+
'npm_config__three_blocks_registry',
|
|
25
|
+
'npm_config_verify_deps_before_run',
|
|
26
|
+
'npm_config_global_bin_dir',
|
|
27
|
+
'npm_config__jsr_registry',
|
|
28
|
+
'npm_config_node_linker',
|
|
29
|
+
].map((key) => key.replace(/-/g, '_').toLowerCase()));
|
|
30
|
+
|
|
31
|
+
const cleanNpmEnv = (extra = {}) => {
|
|
32
|
+
const env = { ...process.env, ...extra };
|
|
33
|
+
for (const key of Object.keys(env)) {
|
|
34
|
+
const normalized = key.replace(/-/g, '_').toLowerCase();
|
|
35
|
+
if (BLOCKED_NPM_ENV_KEYS.has(normalized)) {
|
|
36
|
+
delete env[key];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return env;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const STEP_ICON = '⏺ ';
|
|
43
|
+
const logInfo = (msg) => { console.log(`${STEP_ICON}${msg}`); };
|
|
44
|
+
const logProgress = (msg) => { console.log(`${cyan(STEP_ICON)}${msg}`); };
|
|
45
|
+
const logSuccess = (msg) => { console.log(`${green(STEP_ICON)}${msg}`); };
|
|
46
|
+
const logWarn = (msg) => { console.log(`${yellow(STEP_ICON)}${msg}`); };
|
|
47
|
+
const logError = (msg) => { console.error(`${red(STEP_ICON)}${msg}`); };
|
|
48
|
+
|
|
49
|
+
const die = (m) => { logError(m); process.exit(1); };
|
|
20
50
|
const run = (cmd, args, opts = {}) => {
|
|
21
|
-
const r = spawnSync(cmd, args, { stdio: 'inherit', ...opts });
|
|
51
|
+
const r = spawnSync(cmd, args, { stdio: 'inherit', ...opts, env: cleanNpmEnv(opts.env) });
|
|
22
52
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
23
53
|
};
|
|
24
54
|
|
|
25
55
|
// capture stdout (used for npm pack)
|
|
26
56
|
const exec = (cmd, args, opts = {}) => {
|
|
27
|
-
const r = spawnSync(cmd, args, { stdio: ['ignore', 'pipe', 'inherit'], ...opts });
|
|
57
|
+
const r = spawnSync(cmd, args, { stdio: ['ignore', 'pipe', 'inherit'], ...opts, env: cleanNpmEnv(opts.env) });
|
|
28
58
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
29
59
|
return (r.stdout || Buffer.alloc(0)).toString();
|
|
30
60
|
};
|
|
@@ -88,10 +118,227 @@ function mask(s) {
|
|
|
88
118
|
return v.slice(0, 3) + '••••' + v.slice(-4);
|
|
89
119
|
}
|
|
90
120
|
|
|
121
|
+
const BROKER_DEFAULT_ENDPOINT = process.env.THREE_BLOCKS_BROKER_URL || 'https://www.threejs-blocks.com/api/npm/token';
|
|
122
|
+
|
|
123
|
+
const resolveBrokerEndpoint = (channel) => {
|
|
124
|
+
let endpoint = BROKER_DEFAULT_ENDPOINT;
|
|
125
|
+
try {
|
|
126
|
+
const u = new URL(endpoint);
|
|
127
|
+
if (channel && !u.searchParams.get('channel')) {
|
|
128
|
+
u.searchParams.set('channel', channel);
|
|
129
|
+
}
|
|
130
|
+
endpoint = u.toString();
|
|
131
|
+
} catch {}
|
|
132
|
+
return endpoint;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const fetchTokenMetadata = async (license, channel) => {
|
|
136
|
+
if (!license || typeof fetch !== 'function') return null;
|
|
137
|
+
const endpoint = resolveBrokerEndpoint(channel);
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(endpoint, {
|
|
140
|
+
method: 'GET',
|
|
141
|
+
headers: {
|
|
142
|
+
authorization: `Bearer ${license}`,
|
|
143
|
+
accept: 'application/json',
|
|
144
|
+
'x-three-blocks-channel': channel,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
throw new Error(`HTTP ${res.status}`);
|
|
149
|
+
}
|
|
150
|
+
return await res.json();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
153
|
+
logWarn(`Continuing without token metadata (${msg})`);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const formatRegistryShort = (value) => {
|
|
159
|
+
if (!value) return '';
|
|
160
|
+
try {
|
|
161
|
+
const u = new URL(value);
|
|
162
|
+
const pathname = (u.pathname || '').replace(/\/$/, '');
|
|
163
|
+
return `${u.host}${pathname}`;
|
|
164
|
+
} catch {
|
|
165
|
+
return String(value);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const formatExpiryLabel = (iso) => {
|
|
170
|
+
if (!iso) return 'Expires: —';
|
|
171
|
+
const dt = new Date(iso);
|
|
172
|
+
if (Number.isNaN(dt.getTime())) return `Expires: ${iso}`;
|
|
173
|
+
return `Expires: ${dt.toISOString().replace('T', ' ').replace('Z', 'Z')}`;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const HEADER_WIDTH = 118;
|
|
177
|
+
const LEFT_WIDTH = 72;
|
|
178
|
+
const RIGHT_WIDTH = HEADER_WIDTH - LEFT_WIDTH - 3;
|
|
179
|
+
|
|
180
|
+
const repeatChar = (ch, len) => ch.repeat(Math.max(0, len));
|
|
181
|
+
const stripAnsi = (value) => String(value ?? '').replace(/\u001b\[[0-9;]*m/g, '');
|
|
182
|
+
const ellipsize = (value, width) => {
|
|
183
|
+
const str = String(value ?? '');
|
|
184
|
+
const plain = stripAnsi(str);
|
|
185
|
+
if (plain.length <= width) return str;
|
|
186
|
+
if (width <= 1) return plain.slice(0, width);
|
|
187
|
+
const truncated = plain.slice(0, width - 1) + '…';
|
|
188
|
+
return plain === str ? truncated : truncated;
|
|
189
|
+
};
|
|
190
|
+
const visibleLength = (value) => stripAnsi(String(value ?? '')).length;
|
|
191
|
+
const padText = (value, width, align = 'left') => {
|
|
192
|
+
const text = ellipsize(value, width);
|
|
193
|
+
const remaining = width - visibleLength(text);
|
|
194
|
+
if (remaining <= 0) return text;
|
|
195
|
+
if (align === 'center') {
|
|
196
|
+
const left = Math.floor(remaining / 2);
|
|
197
|
+
const right = remaining - left;
|
|
198
|
+
return `${' '.repeat(left)}${text}${' '.repeat(right)}`;
|
|
199
|
+
}
|
|
200
|
+
if (align === 'right') {
|
|
201
|
+
return `${' '.repeat(remaining)}${text}`;
|
|
202
|
+
}
|
|
203
|
+
return `${text}${' '.repeat(remaining)}`;
|
|
204
|
+
};
|
|
205
|
+
const makeHeaderRow = (left, right = '', leftAlign = 'left', rightAlign = 'left') =>
|
|
206
|
+
`│${padText(left, LEFT_WIDTH, leftAlign)}│${padText(right, RIGHT_WIDTH, rightAlign)}│`;
|
|
207
|
+
|
|
208
|
+
const HEADER_COLOR = ESC(33);
|
|
209
|
+
const CONTENT_COLOR = ESC(90);
|
|
210
|
+
const reapplyColor = (value, color) => {
|
|
211
|
+
const str = String(value ?? '');
|
|
212
|
+
return `${color}${str.split(reset).join(`${reset}${color}`)}${reset}`;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const applyHeaderColor = (row, { keepContentYellow = false, tintContent = true } = {}) => {
|
|
216
|
+
if (keepContentYellow) return reapplyColor(row, HEADER_COLOR);
|
|
217
|
+
if (!row.startsWith('│') || !row.endsWith('│')) return reapplyColor(row, HEADER_COLOR);
|
|
218
|
+
const match = row.match(/^│(.*)│(.*)│$/);
|
|
219
|
+
if (!match) return reapplyColor(row, HEADER_COLOR);
|
|
220
|
+
const [, leftContent, rightContent] = match;
|
|
221
|
+
const leftSegment = tintContent ? reapplyColor(leftContent, CONTENT_COLOR) : leftContent;
|
|
222
|
+
const rightSegment = tintContent ? reapplyColor(rightContent, CONTENT_COLOR) : rightContent;
|
|
223
|
+
return `${HEADER_COLOR}│${reset}${leftSegment}${HEADER_COLOR}│${reset}${rightSegment}${HEADER_COLOR}│${reset}`;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const capitalize = (value) => {
|
|
227
|
+
const str = String(value || '').trim();
|
|
228
|
+
if (!str) return '';
|
|
229
|
+
return str[0].toUpperCase() + str.slice(1);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const formatPlanLabel = (value) => {
|
|
233
|
+
const str = String(value || '').trim();
|
|
234
|
+
if (!str) return '';
|
|
235
|
+
return str
|
|
236
|
+
.split(/\s+/)
|
|
237
|
+
.map((part) => (part ? capitalize(part.toLowerCase()) : ''))
|
|
238
|
+
.join(' ')
|
|
239
|
+
.trim();
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const formatFirstName = (value) => {
|
|
243
|
+
const name = String(value || '').trim();
|
|
244
|
+
if (!name) return '';
|
|
245
|
+
const first = name.split(/\s+/)[0];
|
|
246
|
+
return capitalize(first);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const getUserDisplayName = () => {
|
|
250
|
+
const candidate = process.env.THREE_BLOCKS_USER_NAME
|
|
251
|
+
|| process.env.GIT_AUTHOR_NAME
|
|
252
|
+
|| process.env.USER
|
|
253
|
+
|| process.env.LOGNAME;
|
|
254
|
+
if (candidate) return formatFirstName(candidate);
|
|
255
|
+
try {
|
|
256
|
+
return formatFirstName(os.userInfo().username);
|
|
257
|
+
} catch {
|
|
258
|
+
return '';
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const inferPlan = (license) => {
|
|
263
|
+
if (!license) return 'Unknown plan';
|
|
264
|
+
if (/live/i.test(license)) return 'Pro Plan';
|
|
265
|
+
if (/test/i.test(license)) return 'Sandbox Plan';
|
|
266
|
+
if (/beta/i.test(license)) return 'Beta Plan';
|
|
267
|
+
return 'Developer Plan';
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const extractVersionFromTarball = (filename) => {
|
|
271
|
+
if (!filename) return '';
|
|
272
|
+
const match = filename.match(/-(\d+\.\d+\.\d+(?:-[^.]*)?)\.tgz$/);
|
|
273
|
+
return match ? match[1] : '';
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const renderHeader = ({
|
|
277
|
+
starterVersion,
|
|
278
|
+
userDisplayName,
|
|
279
|
+
planName,
|
|
280
|
+
repoPath,
|
|
281
|
+
projectName,
|
|
282
|
+
channel,
|
|
283
|
+
coreVersion,
|
|
284
|
+
license,
|
|
285
|
+
registry,
|
|
286
|
+
domain,
|
|
287
|
+
region,
|
|
288
|
+
repository,
|
|
289
|
+
expiresAt,
|
|
290
|
+
teamId,
|
|
291
|
+
teamName,
|
|
292
|
+
licenseId,
|
|
293
|
+
}) => {
|
|
294
|
+
const welcome = userDisplayName ? `Welcome back ${userDisplayName}!` : 'Welcome!';
|
|
295
|
+
const normalizedPlan = planName || 'Unknown plan';
|
|
296
|
+
const resolvedTeamName = teamName || teamId || '';
|
|
297
|
+
const planSuffix = resolvedTeamName ? ` · Team: ${resolvedTeamName}` : '';
|
|
298
|
+
const planLine = `@three-blocks/core ${coreVersion || (channel === 'stable' ? 'latest' : channel)} · ${normalizedPlan}${planSuffix}`;
|
|
299
|
+
const channelDisplay = (channel || 'stable').toUpperCase();
|
|
300
|
+
const channelLine = `Channel: ${channelDisplay}${region ? ` · Region: ${region}` : ''}`;
|
|
301
|
+
const repositoryLineBase = repository ? `Repository: ${repository}` : 'Repository: —';
|
|
302
|
+
const registryShort = formatRegistryShort(registry);
|
|
303
|
+
const repositoryLine = registryShort ? `${repositoryLineBase} → ${registryShort}` : repositoryLineBase;
|
|
304
|
+
const registryLine = `Registry: ${registryShort || '—'}`;
|
|
305
|
+
const licenseLine = `License: ${mask(license)}${licenseId ? ` · ${licenseId}` : ''}`;
|
|
306
|
+
const expiresLine = formatExpiryLabel(expiresAt);
|
|
307
|
+
const projectLabel = projectName || path.basename(repoPath);
|
|
308
|
+
const projectLine = `Project: ${projectLabel}`;
|
|
309
|
+
const domainLine = domain ? `Domain: ${domain}` : '';
|
|
310
|
+
const regionLine = region ? `Region: ${region}` : '';
|
|
311
|
+
const title = `─── Three.js Blocks Starter v${starterVersion || '?.?.?'} `;
|
|
312
|
+
const separatorRow = makeHeaderRow(repeatChar('─', LEFT_WIDTH), repeatChar('─', RIGHT_WIDTH));
|
|
313
|
+
const ascii = [
|
|
314
|
+
'THREE.JS',
|
|
315
|
+
' ______ __ ______ ______ __ __ ______ ',
|
|
316
|
+
'/\\ == \\ /\\ \\ /\\ __ \\ /\\ ___\\ /\\ \\/ / /\\ ___\\ ',
|
|
317
|
+
'\\ \\ __< \\ \\ \\____ \\ \\ \\/\\ \\ \\ \\ \\____ \\ \\ _"-. \\ \\___ \\ ',
|
|
318
|
+
' \\ \\_____\\ \\ \\_____\\ \\ \\_____\\ \\ \\_____\\ \\ \\_\\ \\_\\ \\/\\_____\\',
|
|
319
|
+
' \\/_____\/ \\/_____/ \\/_____/ \\/_____/ \\/_/ \/_/ \\/_____/'
|
|
320
|
+
];
|
|
321
|
+
const lines = [
|
|
322
|
+
applyHeaderColor(`╭${title}${repeatChar('─', HEADER_WIDTH - 2 - title.length)}╮`, { keepContentYellow: true }),
|
|
323
|
+
...ascii.map((row) => applyHeaderColor(`│${padText(row, LEFT_WIDTH + RIGHT_WIDTH + 1, 'center')}│`, { keepContentYellow: true })),
|
|
324
|
+
applyHeaderColor(separatorRow, { keepContentYellow: true }),
|
|
325
|
+
applyHeaderColor(makeHeaderRow(welcome, projectLine, 'center', 'center')),
|
|
326
|
+
applyHeaderColor(separatorRow, { keepContentYellow: true }),
|
|
327
|
+
applyHeaderColor(makeHeaderRow(planLine, channelLine)),
|
|
328
|
+
applyHeaderColor(makeHeaderRow(repositoryLine, registryLine)),
|
|
329
|
+
...(domainLine || regionLine ? [applyHeaderColor(makeHeaderRow(domainLine || '', regionLine, 'left', 'center'))] : []),
|
|
330
|
+
applyHeaderColor(makeHeaderRow(licenseLine, expiresLine)),
|
|
331
|
+
applyHeaderColor(`╰${repeatChar('─', HEADER_WIDTH - 2)}╯`, { keepContentYellow: true }),
|
|
332
|
+
];
|
|
333
|
+
for (const row of lines) console.log(row);
|
|
334
|
+
};
|
|
335
|
+
|
|
91
336
|
async function main() {
|
|
92
337
|
const args = process.argv.slice(2);
|
|
93
338
|
let appName = '';
|
|
94
339
|
let channel = String(process.env.THREE_BLOCKS_CHANNEL || 'stable').toLowerCase();
|
|
340
|
+
const userDisplayName = getUserDisplayName();
|
|
341
|
+
const repoPath = process.cwd();
|
|
95
342
|
for (let i = 0; i < args.length; i++) {
|
|
96
343
|
const a = args[i];
|
|
97
344
|
if (!a.startsWith('-') && !appName) { appName = a; continue; }
|
|
@@ -114,21 +361,42 @@ async function main() {
|
|
|
114
361
|
// 1) License key (env or prompt)
|
|
115
362
|
let license = process.env.THREE_BLOCKS_SECRET_KEY;
|
|
116
363
|
if (license) {
|
|
117
|
-
|
|
364
|
+
logInfo(`Using ${bold('THREE_BLOCKS_SECRET_KEY')} from environment ${dim(`(${mask(license)})`)}`);
|
|
118
365
|
} else {
|
|
119
366
|
console.log('');
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
license = await promptHidden(`${
|
|
367
|
+
logInfo(bold(cyan('Three Blocks Starter')) + ' ' + dim(`[channel: ${channel}]`));
|
|
368
|
+
logInfo(dim('Enter your license key to continue.'));
|
|
369
|
+
logInfo(dim('Tip: paste it here; input is hidden. Press Enter to submit.'));
|
|
370
|
+
license = await promptHidden(`${STEP_ICON}${bold('License key')} ${dim('(tb_…)')}: `);
|
|
124
371
|
if (!license) die('License key is required to install private packages.');
|
|
125
372
|
}
|
|
373
|
+
let planName = inferPlan(license);
|
|
374
|
+
const tokenMetadata = await fetchTokenMetadata(license, channel);
|
|
375
|
+
let headerChannel = channel;
|
|
376
|
+
if (tokenMetadata?.channel) {
|
|
377
|
+
const maybeChannel = String(tokenMetadata.channel).toLowerCase();
|
|
378
|
+
if (['stable', 'beta', 'alpha'].includes(maybeChannel)) headerChannel = maybeChannel;
|
|
379
|
+
}
|
|
380
|
+
if (tokenMetadata?.plan) {
|
|
381
|
+
const label = formatPlanLabel(tokenMetadata.plan);
|
|
382
|
+
if (label) {
|
|
383
|
+
planName = label.toLowerCase().includes('plan') ? label : `${label} Plan`;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
let headerRegistry = tokenMetadata?.registry ? String(tokenMetadata.registry) : '';
|
|
387
|
+
const headerDomain = tokenMetadata?.domain ? String(tokenMetadata.domain) : '';
|
|
388
|
+
const headerRegion = tokenMetadata?.region ? String(tokenMetadata.region) : '';
|
|
389
|
+
const headerRepository = tokenMetadata?.repository ? String(tokenMetadata.repository) : '';
|
|
390
|
+
const headerExpires = tokenMetadata?.expiresAt ?? tokenMetadata?.expiresAtIso ?? null;
|
|
391
|
+
const headerTeamId = tokenMetadata?.teamId ? String(tokenMetadata.teamId) : '';
|
|
392
|
+
const headerTeamName = tokenMetadata?.teamName ? String(tokenMetadata.teamName) : '';
|
|
393
|
+
const headerLicenseId = tokenMetadata?.licenseId ? String(tokenMetadata.licenseId) : '';
|
|
126
394
|
|
|
127
395
|
// 2) Pre-login in a TEMP dir to generate a temp .npmrc (no always-auth)
|
|
128
396
|
const tmp = mkTmpDir();
|
|
129
397
|
const tmpNpmrc = path.join(tmp, '.npmrc');
|
|
130
|
-
|
|
131
|
-
run('npx', ['-y',
|
|
398
|
+
logProgress(`Fetching short-lived token (temp .npmrc) [channel: ${channel}] ...`);
|
|
399
|
+
run('npx', ['-y', LOGIN_SPEC, '--mode', 'project', '--scope', SCOPE, '--channel', channel], {
|
|
132
400
|
cwd: tmp,
|
|
133
401
|
env: { ...process.env, THREE_BLOCKS_SECRET_KEY: license, THREE_BLOCKS_CHANNEL: channel },
|
|
134
402
|
});
|
|
@@ -140,10 +408,11 @@ async function main() {
|
|
|
140
408
|
const m = txt.match(/^@[^:]+:registry=(.+)$/m);
|
|
141
409
|
if (m && m[1]) registryUrl = m[1].trim();
|
|
142
410
|
} catch {}
|
|
411
|
+
if (!headerRegistry && registryUrl) headerRegistry = registryUrl;
|
|
143
412
|
|
|
144
413
|
// 3) Scaffold the private starter by packing and extracting the tarball (avoids npm create naming transform)
|
|
145
414
|
const starterSpec = `${STARTER_PKG}@${channel === 'stable' ? 'latest' : channel}`;
|
|
146
|
-
|
|
415
|
+
logProgress(`Fetching starter tarball ${starterSpec} ...`);
|
|
147
416
|
const createEnv = {
|
|
148
417
|
...process.env,
|
|
149
418
|
THREE_BLOCKS_SECRET_KEY: license,
|
|
@@ -152,17 +421,43 @@ async function main() {
|
|
|
152
421
|
};
|
|
153
422
|
let packedOut = exec('npm', ['pack', starterSpec, '--json', '--silent'], { cwd: tmp, env: createEnv });
|
|
154
423
|
let tarName = '';
|
|
424
|
+
let starterVersion = '';
|
|
155
425
|
try {
|
|
156
426
|
const info = JSON.parse(packedOut);
|
|
157
|
-
if (Array.isArray(info))
|
|
158
|
-
|
|
427
|
+
if (Array.isArray(info)) {
|
|
428
|
+
tarName = info[0]?.filename || '';
|
|
429
|
+
starterVersion = info[0]?.version || '';
|
|
430
|
+
} else {
|
|
431
|
+
tarName = info.filename || '';
|
|
432
|
+
starterVersion = info.version || '';
|
|
433
|
+
}
|
|
159
434
|
} catch {
|
|
160
435
|
const lines = String(packedOut || '').trim().split(/\r?\n/);
|
|
161
436
|
tarName = lines[lines.length - 1] || '';
|
|
162
437
|
}
|
|
163
438
|
const tarPath = path.join(tmp, tarName);
|
|
164
439
|
if (!tarName || !fs.existsSync(tarPath)) die('Failed to fetch starter tarball.');
|
|
165
|
-
|
|
440
|
+
const headerStarterVersion = starterVersion || extractVersionFromTarball(tarName);
|
|
441
|
+
const headerCoreVersion = headerChannel === 'stable' ? 'latest' : headerChannel;
|
|
442
|
+
renderHeader({
|
|
443
|
+
starterVersion: headerStarterVersion,
|
|
444
|
+
userDisplayName,
|
|
445
|
+
planName,
|
|
446
|
+
repoPath,
|
|
447
|
+
projectName: appName,
|
|
448
|
+
channel: headerChannel,
|
|
449
|
+
coreVersion: headerCoreVersion,
|
|
450
|
+
license,
|
|
451
|
+
registry: headerRegistry,
|
|
452
|
+
domain: headerDomain,
|
|
453
|
+
region: headerRegion,
|
|
454
|
+
repository: headerRepository,
|
|
455
|
+
expiresAt: headerExpires,
|
|
456
|
+
teamId: headerTeamId,
|
|
457
|
+
teamName: headerTeamName,
|
|
458
|
+
licenseId: headerLicenseId,
|
|
459
|
+
});
|
|
460
|
+
logProgress('Extracting files ...');
|
|
166
461
|
run('tar', ['-xzf', tarPath, '-C', targetDir, '--strip-components=1']);
|
|
167
462
|
|
|
168
463
|
// 4) Write .env.local and .gitignore entries
|
|
@@ -185,15 +480,15 @@ async function main() {
|
|
|
185
480
|
const pkg = JSON.parse(pkgRaw);
|
|
186
481
|
pkg.scripts = pkg.scripts || {};
|
|
187
482
|
if (!pkg.scripts.preinstall) {
|
|
188
|
-
pkg.scripts.preinstall = `npx -y ${LOGIN_CLI} --mode project --scope ${SCOPE} --channel ${channel}`;
|
|
483
|
+
pkg.scripts.preinstall = `npx -y ${LOGIN_CLI}@latest --mode project --scope ${SCOPE} --channel ${channel}`;
|
|
189
484
|
}
|
|
190
485
|
await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
191
|
-
|
|
486
|
+
logSuccess('Added preinstall to generated package.json to refresh token on installs.');
|
|
192
487
|
} catch (e) {
|
|
193
|
-
|
|
488
|
+
logWarn(`Warning: could not add preinstall to package.json: ${e?.message || String(e)}`);
|
|
194
489
|
}
|
|
195
490
|
|
|
196
|
-
|
|
491
|
+
logProgress('Installing dependencies (preinstall will refresh token) ...');
|
|
197
492
|
const hasPnpm = spawnSync('pnpm', ['-v'], { stdio: 'ignore' }).status === 0;
|
|
198
493
|
run(hasPnpm ? 'pnpm' : 'npm', [hasPnpm ? 'install' : 'ci'], {
|
|
199
494
|
cwd: targetDir,
|
|
@@ -206,7 +501,7 @@ async function main() {
|
|
|
206
501
|
|
|
207
502
|
{
|
|
208
503
|
const coreSpec = `@three-blocks/core@${channel === 'stable' ? 'latest' : channel}`;
|
|
209
|
-
|
|
504
|
+
logProgress(`Installing ${coreSpec} ...`);
|
|
210
505
|
const addArgs = hasPnpm ? ['add', '--save-exact', coreSpec] : ['install', '--save-exact', coreSpec];
|
|
211
506
|
const r = spawnSync(hasPnpm ? 'pnpm' : 'npm', addArgs, {
|
|
212
507
|
stdio: 'inherit',
|
|
@@ -218,20 +513,26 @@ async function main() {
|
|
|
218
513
|
},
|
|
219
514
|
});
|
|
220
515
|
if (r.status !== 0) {
|
|
221
|
-
|
|
516
|
+
logWarn(`Warning: could not install @three-blocks/core (exit ${r.status}).`);
|
|
222
517
|
}
|
|
223
518
|
}
|
|
224
519
|
|
|
225
520
|
// 6) Cleanup temp dir
|
|
226
521
|
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
|
|
227
522
|
|
|
228
|
-
console.log(
|
|
229
|
-
|
|
523
|
+
console.log('');
|
|
524
|
+
logSuccess(`${appName} is ready.`);
|
|
525
|
+
console.log('');
|
|
526
|
+
logInfo('Next:');
|
|
230
527
|
console.log(` cd ${appName}`);
|
|
231
528
|
console.log(` ${hasPnpm ? 'pnpm dev' : 'npm run dev'}`);
|
|
232
|
-
console.log(
|
|
529
|
+
console.log('');
|
|
530
|
+
logInfo('Notes:');
|
|
233
531
|
console.log(` • Your license key is stored in .env.local (gitignored).`);
|
|
234
532
|
console.log(` • ${SCOPE} packages are private; each install refreshes a short-lived token via preinstall.`);
|
|
235
533
|
}
|
|
236
534
|
|
|
237
|
-
main().catch((e) => {
|
|
535
|
+
main().catch((e) => {
|
|
536
|
+
logError(e?.stack || e?.message || String(e));
|
|
537
|
+
process.exit(1);
|
|
538
|
+
});
|