create-three-blocks-starter 0.0.3 → 0.0.5

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