@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.
- package/bin/morphite.mjs +105 -158
- 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
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
232
|
+
live.clear()
|
|
233
|
+
header(manifest.name, manifest.version)
|
|
234
|
+
say(grey(`macOS ${macOS} · ${chip}`))
|
|
273
235
|
if (!versionAtLeast(macOS, manifest.minimumMacOS)) {
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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, (
|
|
298
|
+
file = await download(manifest, archive, work, from, (ratio, detail) => live.set(progressLine(ratio, detail)))
|
|
332
299
|
} catch (error) {
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
390
|
-
throw
|
|
347
|
+
if (error.reported) throw error
|
|
348
|
+
throw stop(`${manifest.name} couldn’t be installed.`, `${error.message} Nothing was changed.`)
|
|
391
349
|
}
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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(`\
|
|
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
|
-
|
|
432
|
-
|
|
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
|
})
|