@webprodigies/morphite 0.2.0 → 0.2.1

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/morphite.mjs +105 -158
  2. package/package.json +1 -1
package/bin/morphite.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  /** SOURCE OF TRUTH KEYWORDS: terminalInstaller, installMorphite, INSTALLER_STEPS
3
3
  * WHAT: `npx @webprodigies/morphite` installs or updates the Mac app: checks the Mac, reads the
4
4
  * latest release manifest, downloads the zip for this CPU with progress, checks its SHA-256,
5
- * replaces the app in Applications and opens it with a short getting-started guide.
5
+ * replaces the app in Applications and opens it. It prints a few quiet lines (INSTALLER_LOOK).
6
6
  * WHY: Until Morphite has a Developer ID, a browser download is stopped by Gatekeeper and people must
7
7
  * allow it in Privacy & Security. A file fetched by this program is not marked as downloaded, so the
8
8
  * verified app opens directly. Running the same command again is the update.
@@ -32,88 +32,54 @@ const PACKAGE = '@webprodigies/morphite'
32
32
  const EARLIER_APP_ID = 'com.webprodigies.morphite'
33
33
 
34
34
  // ── Presentation ────────────────────────────────────────────────────────────
35
+ // INSTALLER_LOOK: quiet, Apple-like lines. A bold name, grey details, one thin progress bar with a
36
+ // bold percentage; no logo art, boxes or check marks. One live line updates in place while work runs.
35
37
  const tty = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.TERM !== 'dumb'
36
38
  const paint = (code) => (value) => (tty ? `\x1b[${code}m${value}\x1b[0m` : String(value))
37
39
  const bold = paint('1')
38
- const dim = paint('2')
39
- const grey = (shade) => paint(`38;5;${shade}`)
40
- const green = paint('38;5;114')
41
- const yellow = paint('38;5;221')
42
- const red = paint('38;5;203')
40
+ const grey = paint('38;5;245')
43
41
  const width = () => process.stdout.columns || 80
42
+ const say = (...lines) => { for (const line of lines) console.log(line) }
43
+ /** A heading and a grey explanation, used for results and problems alike. */
44
+ const note = (title, ...details) => say('', bold(title), ...details.map((detail) => grey(detail)))
44
45
 
45
- const LOGO = [
46
- '███╗ ███╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗██╗████████╗███████╗',
47
- '████╗ ████║██╔═══██╗██╔══██╗██╔══██╗██║ ██║██║╚══██╔══╝██╔════╝',
48
- '██╔████╔██║██║ ██║██████╔╝██████╔╝███████║██║ ██║ █████╗ ',
49
- '██║╚██╔╝██║██║ ██║██╔══██╗██╔═══╝ ██╔══██║██║ ██║ ██╔══╝ ',
50
- '██║ ╚═╝ ██║╚██████╔╝██║ ██║██║ ██║ ██║██║ ██║ ███████╗',
51
- '╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝'
52
- ]
53
- function banner(subtitle) {
54
- console.log()
55
- if (width() >= LOGO[0].length + 4) {
56
- const shades = [255, 253, 251, 249, 246, 243]
57
- LOGO.forEach((line, index) => console.log(' ' + grey(shades[index])(line)))
58
- } else console.log(' ' + bold('M O R P H I T E'))
59
- console.log()
60
- console.log(' ' + subtitle)
61
- console.log()
62
- }
63
-
64
- const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
65
- const LABEL = 27
66
- /** One step line: a spinner while it runs, then ✓ with its result (or ✕ and the reason). */
67
- function step(label) {
68
- let frame = 0
69
- let detail = ''
70
- let timer
71
- const line = (mark) => ` ${mark} ${label.padEnd(LABEL)} ${detail}`
72
- const draw = () => {
73
- if (!tty) return
74
- const text = line(grey(245)(FRAMES[(frame = (frame + 1) % FRAMES.length)]))
75
- process.stdout.write('\r\x1b[2K' + text.slice(0, width() + 40))
76
- }
77
- if (tty) timer = setInterval(draw, 80)
78
- draw()
79
- const end = (mark, text) => {
80
- clearInterval(timer)
81
- detail = text ?? detail
82
- process.stdout.write((tty ? '\r\x1b[2K' : '') + line(mark) + '\n')
46
+ /** The one line that changes while work runs; `done` keeps its final text, `clear` removes it. */
47
+ const live = {
48
+ text: '',
49
+ set(text) {
50
+ this.text = text
51
+ if (tty) process.stdout.write('\r\x1b[2K' + text)
52
+ },
53
+ done(text = this.text) {
54
+ if (tty) process.stdout.write('\r\x1b[2K')
55
+ if (text) console.log(text)
56
+ this.text = ''
57
+ },
58
+ clear() {
59
+ if (tty && this.text) process.stdout.write('\r\x1b[2K')
60
+ this.text = ''
83
61
  }
84
- return {
85
- update(text) {
86
- detail = text
87
- draw()
88
- },
89
- done: (text) => end(green('✓'), text && dim(text)),
90
- fail: (text) => end(red('✕'), text && red(text)),
91
- skip: (text) => end(grey(243)('–'), text && dim(text))
92
- }
93
- }
94
- const megabytes = (bytes) => (bytes / 1e6).toFixed(1)
95
- function bar(ratio, size = Math.max(10, Math.min(28, width() - 64))) {
96
- const filled = Math.round(ratio * size)
97
- return grey(252)('━'.repeat(filled)) + grey(238)('━'.repeat(size - filled))
98
62
  }
99
- function box(title, lines) {
100
- const inner = Math.max(title.length + 2, ...lines.map((entry) => entry.plain.length)) + 2
101
- console.log(' ' + grey(243)('╭─ ') + bold(title) + ' ' + grey(243)('─'.repeat(Math.max(0, inner - title.length - 2)) + '╮'))
102
- for (const entry of lines)
103
- console.log(' ' + grey(243)('│') + ' ' + entry.text + ' '.repeat(inner - entry.plain.length - 1) + grey(243)('│'))
104
- console.log(' ' + grey(243)('╰' + '─'.repeat(inner + 1) + '╯'))
63
+ const megabytes = (bytes) => Math.round(bytes / 1e6).toString()
64
+ const BAR = 26
65
+ function bar(ratio) {
66
+ const filled = Math.round(Math.min(1, ratio) * BAR)
67
+ return paint('38;5;253')('━'.repeat(filled)) + paint('38;5;239')('━'.repeat(BAR - filled))
105
68
  }
106
- const row = (plain, text = plain) => ({ plain, text })
69
+ const progressLine = (ratio, detail) => `${bar(ratio)} ${bold(`${Math.floor(Math.min(1, ratio) * 100)}%`)} ${grey(detail)}`
70
+ /** "Morphite (Alpha)" is an Alpha release; a name without a stage is the latest release. */
71
+ const stageOf = (name) => (name.match(/\(([^)]+)\)\s*$/)?.[1] ?? 'Latest') + ' release'
72
+ const header = (name, version) => say('', bold(name), grey(version ? `${version} · ${stageOf(name)}` : 'Installer'), '')
107
73
 
108
74
  /** A yes only when the person types y (or --yes); Return alone, anything else or no keyboard cancels. */
109
75
  async function confirm(question) {
110
76
  if (flag('--yes')) return true
111
77
  if (!process.stdin.isTTY) {
112
- console.log(` ${dim('Run this command in Terminal to answer, or add --yes.')}`)
78
+ say(grey('Run this command in Terminal to answer, or add --yes.'))
113
79
  return false
114
80
  }
115
81
  const prompt = createInterface({ input: process.stdin, output: process.stdout })
116
- const answer = await new Promise((resolve) => prompt.question(` ${yellow('?')} ${question} `, resolve))
82
+ const answer = await new Promise((resolve) => prompt.question(`${bold(question)} ${grey('Type y to continue, anything else cancels')} `, resolve))
117
83
  prompt.close()
118
84
  return ['y', 'yes'].includes(answer.trim().toLowerCase())
119
85
  }
@@ -195,14 +161,7 @@ async function download(manifest, archive, folder, from, progress) {
195
161
  const destination = join(folder, archive.file)
196
162
  const hash = createHash('sha256')
197
163
  let received = 0
198
- const started = Date.now()
199
- const report = () => {
200
- const seconds = Math.max(0.25, (Date.now() - started) / 1000)
201
- progress(
202
- `${bar(received / archive.size)} ${String(Math.floor((received / archive.size) * 100)).padStart(3)}% ` +
203
- dim(`${megabytes(received)} / ${megabytes(archive.size)} MB · ${megabytes(received / seconds)} MB/s`)
204
- )
205
- }
164
+ const report = () => progress(received / archive.size, `${megabytes(received)} of ${megabytes(archive.size)} MB`)
206
165
  const output = createWriteStream(destination, { mode: 0o600 })
207
166
  const write = async (chunk) => {
208
167
  hash.update(chunk)
@@ -226,7 +185,6 @@ async function download(manifest, archive, folder, from, progress) {
226
185
  clearInterval(timer)
227
186
  output.destroy()
228
187
  }
229
- report()
230
188
  return { path: destination, sha256: hash.digest('hex'), size: received }
231
189
  }
232
190
 
@@ -239,46 +197,51 @@ async function writable(folder) {
239
197
  }
240
198
  async function main() {
241
199
  if (flag('--help') || flag('-h')) {
242
- console.log(`\n ${bold('npx ' + PACKAGE)} Install or update Morphite on this Mac.\n\n --yes Answer yes to every question\n --no-open Do not open Morphite afterwards\n --reinstall Install again even when this version is already installed\n --from DIR Install from a local release folder (for testing a release before publishing)\n`)
200
+ say('', bold('npx ' + PACKAGE), grey('Installs or updates Morphite on this Mac.'), '',
201
+ `--yes ${grey('Answer yes to every question')}`,
202
+ `--no-open ${grey('Do not open Morphite afterwards')}`,
203
+ `--reinstall ${grey('Install again even when this version is installed')}`,
204
+ `--from DIR ${grey('Install from a local release folder, to test a release before publishing')}`, '')
243
205
  return
244
206
  }
245
207
  const from = option('--from')
246
208
  if (platform() !== 'darwin') {
247
- banner(bold('Morphite') + dim(' installer'))
248
- console.log(` ${yellow('!')} Morphite runs on macOS for now. Windows and Linux are on the way.`)
249
- console.log(` ${dim('Follow along at https://morphite.build')}\n`)
209
+ header('Morphite')
210
+ note('Morphite runs on macOS for now.', 'Windows and Linux are on the way. Follow along at https://morphite.build')
211
+ say('')
250
212
  process.exitCode = 1
251
213
  return
252
214
  }
253
215
 
254
- const checking = step('Checking your Mac')
216
+ live.set(grey('Checking for the latest release…'))
255
217
  const macOS = (await run('/usr/bin/sw_vers', ['-productVersion'])).stdout.trim()
256
218
  const cpu = await macCpu()
257
- checking.update(dim(`macOS ${macOS} · ${cpu === 'arm64' ? 'Apple silicon' : 'Intel'}`))
258
-
259
- // The banner names the app from the release, so read it before printing anything else.
219
+ const chip = cpu === 'arm64' ? 'Apple silicon' : 'Intel'
220
+ // The header names the app and version from the release, so read it before printing anything else.
260
221
  let manifest
261
222
  try {
262
223
  manifest = await readManifest(from)
263
224
  } catch (error) {
264
- checking.skip()
265
- banner(bold('Morphite') + dim(' installer'))
266
- console.log(` ${red('✕')} ${error.message}\n`)
225
+ live.clear()
226
+ header('Morphite')
227
+ note('Morphite couldn’t be installed.', error.message)
228
+ say('')
267
229
  process.exitCode = 1
268
230
  return
269
231
  }
270
- process.stdout.write(tty ? '\r\x1b[2K' : '')
271
- banner(`${bold(manifest.name)} ${dim(`${manifest.version} · installer`)}`)
272
- checking.done(`macOS ${macOS} · ${cpu === 'arm64' ? 'Apple silicon' : 'Intel'}`)
232
+ live.clear()
233
+ header(manifest.name, manifest.version)
234
+ say(grey(`macOS ${macOS} · ${chip}`))
273
235
  if (!versionAtLeast(macOS, manifest.minimumMacOS)) {
274
- console.log(`\n ${red('✕')} ${manifest.name} needs macOS ${manifest.minimumMacOS} or later. This Mac has ${macOS}.\n`)
236
+ note(`${manifest.name} needs macOS ${manifest.minimumMacOS} or later.`, `This Mac has macOS ${macOS}.`)
237
+ say('')
275
238
  process.exitCode = 1
276
239
  return
277
240
  }
278
241
  const archive = manifest.archives[cpu]
279
- const release = step('Finding the latest release')
280
242
  if (!archive) {
281
- release.fail(`No ${cpu === 'arm64' ? 'Apple silicon' : 'Intel'} build in this release.`)
243
+ note(`This release has no ${chip} build yet.`, 'Nothing was changed.')
244
+ say('')
282
245
  process.exitCode = 1
283
246
  return
284
247
  }
@@ -286,91 +249,86 @@ async function main() {
286
249
  const personal = join(homedir(), 'Applications')
287
250
  const installed = await appsWithId([applications, personal], manifest.bundleId)
288
251
  const current = installed.length ? await versionOf(installed[0]) : null
289
- release.done(`${manifest.name} ${manifest.version}${current ? ` (installed: ${current})` : ''}`)
290
252
  const earlier = await appsWithId([applications, personal], EARLIER_APP_ID)
291
253
 
292
254
  // Already current (and nothing earlier to retire): say so and open it, without downloading again.
293
255
  if (current === manifest.version && !earlier.length && !flag('--reinstall')) {
294
- if (!flag('--no-open')) await run('/usr/bin/open', [installed[0]]).catch(() => undefined)
295
- console.log(`\n ${green('✓')} ${manifest.name} ${manifest.version} is already installed ${dim(`(${tilde(installed[0])})`)}\n`)
256
+ const opened = !flag('--no-open') && (await run('/usr/bin/open', [installed[0]]).then(() => true, () => false))
257
+ note('You’re up to date.', `${manifest.name} ${manifest.version} is installed in ${dirname(installed[0]) === '/Applications' ? 'Applications' : tilde(dirname(installed[0]))}.${opened ? ' It’s open now.' : ''}`)
258
+ say('')
296
259
  return
297
260
  }
298
261
 
299
262
  // The earlier Morphite app (com.webprodigies.morphite): a new app replaces it and starts fresh.
300
263
  // Promise nothing about its data; this installer only moves the app itself to the Trash.
301
264
  if (earlier.length) {
302
- const version = await versionOf(earlier[0])
303
- console.log()
304
- console.log(` ${yellow('!')} Found ${bold(['Morphite', version].filter(Boolean).join(' '))} ${dim(`in ${earlier.map((app) => tilde(dirname(app))).join(', ')}`)}`)
305
- console.log()
306
- console.log(` ${manifest.name} is a brand-new app. It replaces the earlier Morphite and starts fresh:`)
307
- console.log(` ${dim('•')} projects, chats and settings from the earlier app do not carry over`)
308
- console.log(` ${dim('•')} the earlier app moves to the Trash`)
309
- console.log(` ${dim('•')} your code and project folders are not touched`)
310
- console.log()
311
- console.log(` If there is anything from the earlier app you want to keep, back it up before continuing.`)
312
- console.log()
313
- if (!(await confirm(`Type ${bold('y')} and press Return to continue, or anything else to cancel:`))) {
314
- console.log(`\n Cancelled. Nothing was changed.\n`)
265
+ note(
266
+ 'An earlier Morphite is on this Mac.',
267
+ `${manifest.name} is a new app and starts fresh. Projects, chats and`,
268
+ 'settings from the earlier app don’t carry over; it moves to the Trash.',
269
+ 'Your code and project folders aren’t touched. Back up anything first.'
270
+ )
271
+ say('')
272
+ if (!(await confirm('Continue?'))) {
273
+ note('Cancelled.', 'Nothing was changed.')
274
+ say('')
315
275
  return
316
276
  }
277
+ say('')
317
278
  }
318
279
 
319
280
  const target = (await writable(applications)) ? applications : personal
281
+ const place = target === applications ? 'Applications' : tilde(target)
320
282
  const destination = join(target, `${manifest.name}.app`)
321
283
  const work = await mkdtemp(join(tmpdir(), 'morphite-installer-'))
322
284
  const cleanup = () => rm(work, { recursive: true, force: true }).catch(() => undefined)
323
285
  process.once('SIGINT', () => {
324
- process.stdout.write('\n')
286
+ live.done()
325
287
  void cleanup().then(() => process.exit(130))
326
288
  })
289
+ // A problem after the download started: keep the last line where it stopped, then say what happened.
290
+ const stop = (title, detail) => {
291
+ live.done()
292
+ note(title, detail)
293
+ return Object.assign(Error(title), { reported: true })
294
+ }
327
295
  try {
328
- const downloading = step('Downloading')
329
296
  let file
330
297
  try {
331
- file = await download(manifest, archive, work, from, (text) => downloading.update(text))
298
+ file = await download(manifest, archive, work, from, (ratio, detail) => live.set(progressLine(ratio, detail)))
332
299
  } catch (error) {
333
- downloading.fail(error.message)
334
- throw Object.assign(error, { reported: true })
335
- }
336
- downloading.done(`${megabytes(file.size)} MB`)
337
-
338
- const verifying = step('Verifying')
339
- if (file.size !== archive.size || file.sha256 !== archive.sha256) {
340
- verifying.fail('The download does not match the release checksum. Nothing was installed.')
341
- throw Object.assign(Error('checksum'), { reported: true })
300
+ throw stop('The download stopped.', `${error.message} Nothing was changed.`)
342
301
  }
343
- verifying.done('SHA-256 matches the release')
302
+ if (file.size !== archive.size || file.sha256 !== archive.sha256)
303
+ throw stop('The download doesn’t match the release.', 'Nothing was changed. Run the command again.')
304
+ live.done(progressLine(1, `${megabytes(file.size)} MB · verified`))
344
305
 
345
- const unpacking = step('Unpacking')
306
+ live.set(grey(`${current ? 'Updating' : 'Installing'} in ${place}…`))
346
307
  await mkdir(target, { recursive: true })
347
308
  // Stage beside the destination so the final move is a rename on the same disk.
348
309
  const stage = await mkdtemp(join(target, '.morphite-install-'))
349
310
  try {
350
311
  await run('/usr/bin/ditto', ['-x', '-k', file.path, stage])
351
312
  const candidate = join(stage, `${manifest.name}.app`)
352
- if ((await bundleIdOf(candidate)) !== manifest.bundleId) throw Error('The downloaded app is not the expected Morphite build.')
313
+ if ((await bundleIdOf(candidate)) !== manifest.bundleId) throw stop('The downloaded app isn’t the expected Morphite build.', 'Nothing was changed.')
353
314
  await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', candidate]).catch(() => {
354
- throw Error('The downloaded app failed its signature check. Nothing was installed.')
315
+ throw stop('The downloaded app failed its signature check.', 'Nothing was changed.')
355
316
  })
356
317
  // Fetched by this program, the app carries no download mark; clear one defensively all the same.
357
318
  await run('/usr/bin/xattr', ['-dr', 'com.apple.quarantine', candidate]).catch(() => undefined)
358
- unpacking.done(manifest.name + '.app')
359
319
 
360
320
  // Morphite must be closed before its app is replaced; the person closes it, never this script.
361
321
  let open = await runningFrom([...installed, ...earlier])
362
322
  if (open.length) {
363
323
  const names = open.map((app) => basename(app, '.app')).join(' and ')
364
- const waiting = step(`Closing ${names}`)
365
- waiting.update(yellow(`Quit ${names} (⌘Q) to continue`))
324
+ live.set(`${bold(`Quit ${names} to continue`)} ${grey('(⌘Q)')}`)
366
325
  while (open.length) {
367
326
  await new Promise((resolve) => setTimeout(resolve, 700))
368
327
  open = await runningFrom([...installed, ...earlier])
369
328
  }
370
- waiting.done('closed')
329
+ live.set(grey(`${current ? 'Updating' : 'Installing'} in ${place}…`))
371
330
  }
372
331
 
373
- const installing = step(current ? 'Updating' : 'Installing')
374
332
  const replaced = []
375
333
  try {
376
334
  // Set the old app aside in the staging folder; it is removed with that folder only once the
@@ -381,42 +339,30 @@ async function main() {
381
339
  await rename(previous, aside)
382
340
  replaced.push([previous, aside])
383
341
  }
384
- if (await stat(destination).catch(() => null)) throw Error(`${destination} already exists and is not ${manifest.name}.`)
342
+ if (await stat(destination).catch(() => null)) throw Error(`${tilde(destination)} already exists and isn’t ${manifest.name}.`)
385
343
  await rename(candidate, destination)
386
344
  } catch (error) {
387
345
  // Put every earlier copy back where it was; the install did not happen.
388
346
  for (const [original, moved] of replaced.reverse()) await rename(moved, original).catch(() => undefined)
389
- installing.fail(error.message)
390
- throw Object.assign(error, { reported: true })
347
+ if (error.reported) throw error
348
+ throw stop(`${manifest.name} couldn’t be installed.`, `${error.message} Nothing was changed.`)
391
349
  }
392
- installing.done(destination.replace(homedir(), '~'))
350
+ live.done(grey(`${current ? 'Updated' : 'Installed'} in ${place}`))
393
351
  } finally {
394
352
  await rm(stage, { recursive: true, force: true }).catch(() => undefined)
395
353
  }
396
354
 
397
355
  // Agreed above: the earlier app goes to the Trash (recoverable); its data folder stays.
398
- for (const app of earlier) {
399
- const retiring = step('Retiring the earlier app')
400
- if (await moveToTrash(app)) retiring.done(`${basename(app)} moved to the Trash`)
401
- else retiring.skip(`drag ${basename(app)} from Applications to the Trash`)
402
- }
356
+ for (const app of earlier)
357
+ say(grey((await moveToTrash(app)) ? 'Moved the earlier Morphite to the Trash' : `Drag ${basename(app)} from ${tilde(dirname(app))} to the Trash`))
403
358
 
404
- if (!flag('--no-open')) {
405
- const opening = step(`Opening ${manifest.name}`)
406
- await run('/usr/bin/open', [destination]).then(
407
- () => opening.done(),
408
- () => opening.skip('open it from Applications')
409
- )
410
- }
411
- console.log()
412
- box(current ? `${manifest.name} is up to date` : `Welcome to ${manifest.name}`, [
413
- row('1 Sign in with your Morphite account (your browser opens).', `${bold('1')} Sign in with your Morphite account ${dim('(your browser opens)')}.`),
414
- row('2 Connect Claude Code, Codex or another coding agent.', `${bold('2')} Connect Claude Code, Codex or another coding agent.`),
415
- row('3 Open a project folder, start a room and say what to build.', `${bold('3')} Open a project folder, start a room and say what to build.`),
416
- row(''),
417
- row(`Update any time: npx ${PACKAGE}@latest`, `${dim('Update any time:')} ${bold(`npx ${PACKAGE}@latest`)}`)
418
- ])
419
- console.log()
359
+ const opened = !flag('--no-open') && (await run('/usr/bin/open', [destination]).then(() => true, () => false))
360
+ note(
361
+ `${manifest.name} is ready.`,
362
+ opened ? `It’s open now, and in ${place} and Spotlight.` : `Open it from ${place} or Spotlight.`,
363
+ 'Run this command again to update.'
364
+ )
365
+ say('')
420
366
  } finally {
421
367
  await cleanup()
422
368
  }
@@ -424,11 +370,12 @@ async function main() {
424
370
 
425
371
  const [major] = process.versions.node.split('.').map(Number)
426
372
  if (major < 18) {
427
- console.error(`\n Morphite's installer needs Node.js 18 or later (you have ${process.versions.node}). Get it from https://nodejs.org\n`)
373
+ console.error(`\nMorphite’s installer needs Node.js 18 or later (you have ${process.versions.node}). Get it from https://nodejs.org\n`)
428
374
  process.exit(1)
429
375
  }
430
376
  main().catch((error) => {
431
- if (!error?.reported) console.error(`\n ${red('✕')} ${error?.message ?? error}\n`)
432
- else console.log()
377
+ live.done()
378
+ if (!error?.reported) note('Morphite couldn’t be installed.', String(error?.message ?? error))
379
+ say('')
433
380
  process.exitCode = 1
434
381
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webprodigies/morphite",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Install or update Morphite on your Mac: npx @webprodigies/morphite",
5
5
  "type": "module",
6
6
  "bin": {