@webprodigies/morphite 0.2.0 → 0.2.2
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 +148 -164
- 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')
|
|
83
|
-
}
|
|
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))
|
|
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 = ''
|
|
92
61
|
}
|
|
93
62
|
}
|
|
94
|
-
const megabytes = (bytes) => (bytes / 1e6).
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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))
|
|
98
68
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.log(' ' + grey(243)('│') + ' ' + entry.text + ' '.repeat(inner - entry.plain.length - 1) + grey(243)('│'))
|
|
104
|
-
console.log(' ' + grey(243)('╰' + '─'.repeat(inner + 1) + '╯'))
|
|
105
|
-
}
|
|
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
|
-
|
|
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,123 @@ 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
|
-
|
|
324
|
-
|
|
285
|
+
const stopped = () => {
|
|
286
|
+
live.done()
|
|
287
|
+
note('Stopped. Nothing was changed.', `Run npx ${PACKAGE}@latest again when you’re ready.`)
|
|
288
|
+
say('')
|
|
325
289
|
void cleanup().then(() => process.exit(130))
|
|
326
|
-
}
|
|
290
|
+
}
|
|
291
|
+
process.once('SIGINT', stopped)
|
|
292
|
+
// A problem after the download started: keep the last line where it stopped, then say what happened.
|
|
293
|
+
const stop = (title, detail) => {
|
|
294
|
+
live.done()
|
|
295
|
+
note(title, detail)
|
|
296
|
+
return Object.assign(Error(title), { reported: true })
|
|
297
|
+
}
|
|
327
298
|
try {
|
|
328
|
-
const downloading = step('Downloading')
|
|
329
299
|
let file
|
|
330
300
|
try {
|
|
331
|
-
file = await download(manifest, archive, work, from, (
|
|
301
|
+
file = await download(manifest, archive, work, from, (ratio, detail) => live.set(progressLine(ratio, detail)))
|
|
332
302
|
} catch (error) {
|
|
333
|
-
|
|
334
|
-
throw Object.assign(error, { reported: true })
|
|
303
|
+
throw stop('The download stopped.', `${error.message} Nothing was changed.`)
|
|
335
304
|
}
|
|
336
|
-
|
|
305
|
+
if (file.size !== archive.size || file.sha256 !== archive.sha256)
|
|
306
|
+
throw stop('The download doesn’t match the release.', 'Nothing was changed. Run the command again.')
|
|
307
|
+
live.done(progressLine(1, `${megabytes(file.size)} MB · verified`))
|
|
337
308
|
|
|
338
|
-
|
|
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 })
|
|
342
|
-
}
|
|
343
|
-
verifying.done('SHA-256 matches the release')
|
|
344
|
-
|
|
345
|
-
const unpacking = step('Unpacking')
|
|
309
|
+
live.set(grey(`${current ? 'Updating' : 'Installing'} in ${place}…`))
|
|
346
310
|
await mkdir(target, { recursive: true })
|
|
347
311
|
// Stage beside the destination so the final move is a rename on the same disk.
|
|
348
312
|
const stage = await mkdtemp(join(target, '.morphite-install-'))
|
|
349
313
|
try {
|
|
350
314
|
await run('/usr/bin/ditto', ['-x', '-k', file.path, stage])
|
|
351
315
|
const candidate = join(stage, `${manifest.name}.app`)
|
|
352
|
-
if ((await bundleIdOf(candidate)) !== manifest.bundleId) throw
|
|
316
|
+
if ((await bundleIdOf(candidate)) !== manifest.bundleId) throw stop('The downloaded app isn’t the expected Morphite build.', 'Nothing was changed.')
|
|
353
317
|
await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', candidate]).catch(() => {
|
|
354
|
-
throw
|
|
318
|
+
throw stop('The downloaded app failed its signature check.', 'Nothing was changed.')
|
|
355
319
|
})
|
|
356
320
|
// Fetched by this program, the app carries no download mark; clear one defensively all the same.
|
|
357
321
|
await run('/usr/bin/xattr', ['-dr', 'com.apple.quarantine', candidate]).catch(() => undefined)
|
|
358
|
-
unpacking.done(manifest.name + '.app')
|
|
359
322
|
|
|
360
|
-
// Morphite must be closed before its app is replaced
|
|
323
|
+
// Morphite must be closed before its app is replaced. The person quits it from its menu, or
|
|
324
|
+
// presses Enter and the installer asks it to quit the normal way (never a forced stop, so its
|
|
325
|
+
// work ends cleanly). No keyboard shortcut is suggested: ⌘Q here would quit Terminal.
|
|
361
326
|
let open = await runningFrom([...installed, ...earlier])
|
|
362
327
|
if (open.length) {
|
|
363
328
|
const names = open.map((app) => basename(app, '.app')).join(' and ')
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
329
|
+
live.clear()
|
|
330
|
+
note(
|
|
331
|
+
`${names} is open.`,
|
|
332
|
+
`Quit it from its menu bar (${names} → Quit), and this ${current ? 'update' : 'install'} continues on its own.`,
|
|
333
|
+
tty ? 'Or press Return to quit it now. Control-C stops without changing anything.' : 'Control-C stops without changing anything.'
|
|
334
|
+
)
|
|
335
|
+
say('')
|
|
336
|
+
const input = tty && process.stdin.isTTY ? process.stdin : null
|
|
337
|
+
const key = (data) => {
|
|
338
|
+
const text = String(data)
|
|
339
|
+
if (text.includes('\u0003')) return stopped()
|
|
340
|
+
if (!/[\r\n]/.test(text)) return
|
|
341
|
+
live.set(grey(`Quitting ${names}…`))
|
|
342
|
+
for (const app of open) {
|
|
343
|
+
const id = earlier.includes(app) ? EARLIER_APP_ID : manifest.bundleId
|
|
344
|
+
void run('/usr/bin/osascript', ['-e', `tell application id ${JSON.stringify(id)} to quit`]).catch(() => undefined)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (input) {
|
|
348
|
+
input.setRawMode(true)
|
|
349
|
+
input.resume()
|
|
350
|
+
input.on('data', key)
|
|
369
351
|
}
|
|
370
|
-
|
|
352
|
+
live.set(grey(`Waiting for ${names} to quit…`))
|
|
353
|
+
try {
|
|
354
|
+
while (open.length) {
|
|
355
|
+
await new Promise((resolve) => setTimeout(resolve, 700))
|
|
356
|
+
open = await runningFrom([...installed, ...earlier])
|
|
357
|
+
}
|
|
358
|
+
} finally {
|
|
359
|
+
if (input) {
|
|
360
|
+
input.off('data', key)
|
|
361
|
+
input.setRawMode(false)
|
|
362
|
+
input.pause()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
live.done(grey(`${names} quit`))
|
|
366
|
+
live.set(grey(`${current ? 'Updating' : 'Installing'} in ${place}…`))
|
|
371
367
|
}
|
|
372
368
|
|
|
373
|
-
const installing = step(current ? 'Updating' : 'Installing')
|
|
374
369
|
const replaced = []
|
|
375
370
|
try {
|
|
376
371
|
// Set the old app aside in the staging folder; it is removed with that folder only once the
|
|
@@ -381,42 +376,30 @@ async function main() {
|
|
|
381
376
|
await rename(previous, aside)
|
|
382
377
|
replaced.push([previous, aside])
|
|
383
378
|
}
|
|
384
|
-
if (await stat(destination).catch(() => null)) throw Error(`${destination} already exists and
|
|
379
|
+
if (await stat(destination).catch(() => null)) throw Error(`${tilde(destination)} already exists and isn’t ${manifest.name}.`)
|
|
385
380
|
await rename(candidate, destination)
|
|
386
381
|
} catch (error) {
|
|
387
382
|
// Put every earlier copy back where it was; the install did not happen.
|
|
388
383
|
for (const [original, moved] of replaced.reverse()) await rename(moved, original).catch(() => undefined)
|
|
389
|
-
|
|
390
|
-
throw
|
|
384
|
+
if (error.reported) throw error
|
|
385
|
+
throw stop(`${manifest.name} couldn’t be installed.`, `${error.message} Nothing was changed.`)
|
|
391
386
|
}
|
|
392
|
-
|
|
387
|
+
live.done(grey(`${current ? 'Updated' : 'Installed'} in ${place}`))
|
|
393
388
|
} finally {
|
|
394
389
|
await rm(stage, { recursive: true, force: true }).catch(() => undefined)
|
|
395
390
|
}
|
|
396
391
|
|
|
397
392
|
// 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
|
-
}
|
|
393
|
+
for (const app of earlier)
|
|
394
|
+
say(grey((await moveToTrash(app)) ? 'Moved the earlier Morphite to the Trash' : `Drag ${basename(app)} from ${tilde(dirname(app))} to the Trash`))
|
|
403
395
|
|
|
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()
|
|
396
|
+
const opened = !flag('--no-open') && (await run('/usr/bin/open', [destination]).then(() => true, () => false))
|
|
397
|
+
note(
|
|
398
|
+
`${manifest.name} is ready.`,
|
|
399
|
+
opened ? `It’s open now, and in ${place} and Spotlight.` : `Open it from ${place} or Spotlight.`,
|
|
400
|
+
'Run this command again to update.'
|
|
401
|
+
)
|
|
402
|
+
say('')
|
|
420
403
|
} finally {
|
|
421
404
|
await cleanup()
|
|
422
405
|
}
|
|
@@ -424,11 +407,12 @@ async function main() {
|
|
|
424
407
|
|
|
425
408
|
const [major] = process.versions.node.split('.').map(Number)
|
|
426
409
|
if (major < 18) {
|
|
427
|
-
console.error(`\
|
|
410
|
+
console.error(`\nMorphite’s installer needs Node.js 18 or later (you have ${process.versions.node}). Get it from https://nodejs.org\n`)
|
|
428
411
|
process.exit(1)
|
|
429
412
|
}
|
|
430
413
|
main().catch((error) => {
|
|
431
|
-
|
|
432
|
-
|
|
414
|
+
live.done()
|
|
415
|
+
if (!error?.reported) note('Morphite couldn’t be installed.', String(error?.message ?? error))
|
|
416
|
+
say('')
|
|
433
417
|
process.exitCode = 1
|
|
434
418
|
})
|