@folotoy/folotoy-openclaw-plugin 0.6.2 → 0.8.0
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/README.md +23 -2
- package/dist/cli/install.js +39 -6
- package/dist/cli/install.js.map +1 -1
- package/dist/cli/package-info.d.ts +12 -0
- package/dist/cli/package-info.d.ts.map +1 -0
- package/dist/cli/package-info.js +42 -0
- package/dist/cli/package-info.js.map +1 -0
- package/dist/cli/preset.d.ts +6 -0
- package/dist/cli/preset.d.ts.map +1 -0
- package/dist/cli/preset.js +45 -0
- package/dist/cli/preset.js.map +1 -0
- package/dist/config-schema.d.ts +33 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config-schema.js +50 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -37
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +100 -41
- package/package.json +9 -8
- package/src/config-schema.ts +71 -0
- package/src/config.ts +1 -1
- package/src/index.ts +25 -37
- package/bin/folotoy.mjs +0 -2
- package/src/__tests__/channel.test.ts +0 -126
- package/src/__tests__/mqtt.test.ts +0 -14
- package/src/__tests__/soothing.test.ts +0 -47
- package/src/__tests__/test-message.mjs +0 -233
- package/src/cli/install.ts +0 -157
- package/src/cli/qrcode-terminal.d.ts +0 -3
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MQTT integration test — simulates a FoloToy toy.
|
|
3
|
-
*
|
|
4
|
-
* Modes:
|
|
5
|
-
* chat — send a message, verify outbound reply format (soothing + reply + finish)
|
|
6
|
-
* reminder — send a reminder request, verify reply, wait for notification delivery
|
|
7
|
-
*
|
|
8
|
-
* Configuration (via .env or environment variables):
|
|
9
|
-
* FOLOTOY_TOY_SN — toy serial number
|
|
10
|
-
* FOLOTOY_TOY_KEY — toy key (MQTT password)
|
|
11
|
-
* FOLOTOY_MQTT_HOST — MQTT broker host
|
|
12
|
-
* FOLOTOY_MQTT_PORT — MQTT broker port (default 1883)
|
|
13
|
-
* FOLOTOY_TEST_MQTT_USERNAME — MQTT username for test client (default: FOLOTOY_TOY_SN)
|
|
14
|
-
* FOLOTOY_TEST_MQTT_PASSWORD — MQTT password for test client (default: FOLOTOY_TOY_KEY)
|
|
15
|
-
*
|
|
16
|
-
* Usage:
|
|
17
|
-
* node --env-file=.env test-mqtt.mjs chat "你好"
|
|
18
|
-
* node --env-file=.env test-mqtt.mjs reminder
|
|
19
|
-
* node --env-file=.env test-mqtt.mjs # defaults to: chat "你好"
|
|
20
|
-
*/
|
|
21
|
-
import mqtt from 'mqtt'
|
|
22
|
-
|
|
23
|
-
const SN = process.env.FOLOTOY_TOY_SN
|
|
24
|
-
const KEY = process.env.FOLOTOY_TOY_KEY
|
|
25
|
-
const HOST = process.env.FOLOTOY_MQTT_HOST
|
|
26
|
-
const PORT = Number(process.env.FOLOTOY_MQTT_PORT || 1883)
|
|
27
|
-
|
|
28
|
-
if (!SN || !HOST) {
|
|
29
|
-
console.error('Missing FOLOTOY_TOY_SN or FOLOTOY_MQTT_HOST. Set them in .env or as env vars.')
|
|
30
|
-
process.exit(1)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Parse args
|
|
34
|
-
const args = process.argv.slice(2)
|
|
35
|
-
let mode = 'chat'
|
|
36
|
-
let message = '你好'
|
|
37
|
-
|
|
38
|
-
if (args[0] === 'chat') {
|
|
39
|
-
mode = 'chat'
|
|
40
|
-
message = args[1] || '你好'
|
|
41
|
-
} else if (args[0] === 'reminder') {
|
|
42
|
-
mode = 'reminder'
|
|
43
|
-
message = args[1] || '1分钟后提醒我站起来'
|
|
44
|
-
} else if (args[0]) {
|
|
45
|
-
message = args[0]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const inboundTopic = `/openapi/folotoy/${SN}/thing/command/call`
|
|
49
|
-
const outboundTopic = `/openapi/folotoy/${SN}/thing/command/callAck`
|
|
50
|
-
const notificationTopic = `/openapi/folotoy/${SN}/thing/event/post`
|
|
51
|
-
|
|
52
|
-
// ── Validation state ──
|
|
53
|
-
const errors = []
|
|
54
|
-
let gotSoothing = false
|
|
55
|
-
let gotReply = false
|
|
56
|
-
let gotFinish = false
|
|
57
|
-
let gotNotification = false
|
|
58
|
-
let soothingCount = 0
|
|
59
|
-
let firstReplyAt = 0
|
|
60
|
-
const soothingTimestamps = []
|
|
61
|
-
const replyTimestamps = []
|
|
62
|
-
const startTime = Date.now()
|
|
63
|
-
|
|
64
|
-
function validateOutboundMessage(msg) {
|
|
65
|
-
if (msg.identifier !== 'chat_output') {
|
|
66
|
-
errors.push(`Expected identifier "chat_output", got "${msg.identifier}"`)
|
|
67
|
-
}
|
|
68
|
-
const p = msg.outParams
|
|
69
|
-
if (typeof p?.recording_id !== 'number') {
|
|
70
|
-
errors.push(`Missing or invalid recording_id in outbound message (order=${p?.order})`)
|
|
71
|
-
}
|
|
72
|
-
if (typeof p?.order !== 'number') {
|
|
73
|
-
errors.push('Missing or invalid order in outbound message')
|
|
74
|
-
}
|
|
75
|
-
if (typeof p?.is_finished !== 'boolean') {
|
|
76
|
-
errors.push(`Missing or invalid is_finished in outbound message (order=${p?.order})`)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function validateNotificationMessage(msg) {
|
|
81
|
-
if (msg.identifier !== 'send_notification') {
|
|
82
|
-
errors.push(`Expected notification identifier "send_notification", got "${msg.identifier}"`)
|
|
83
|
-
}
|
|
84
|
-
if (typeof msg.outParams?.text !== 'string' || !msg.outParams.text) {
|
|
85
|
-
errors.push('Notification missing outParams.text')
|
|
86
|
-
}
|
|
87
|
-
if ('recording_id' in (msg.outParams || {})) {
|
|
88
|
-
errors.push('Notification should not contain recording_id')
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function printSummary() {
|
|
93
|
-
console.log('\n' + '='.repeat(60))
|
|
94
|
-
console.log(`TEST SUMMARY (mode: ${mode})`)
|
|
95
|
-
console.log('='.repeat(60))
|
|
96
|
-
|
|
97
|
-
const checks = [
|
|
98
|
-
['Soothing ack received', gotSoothing],
|
|
99
|
-
['AI reply received', gotReply],
|
|
100
|
-
['Finish message (is_finished=true)', gotFinish],
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
for (const [label, ok] of checks) {
|
|
104
|
-
console.log(` ${ok ? '✅' : '❌'} ${label}`)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
console.log('\n Soothing stats:')
|
|
108
|
-
console.log(` Total soothing messages: ${soothingCount}`)
|
|
109
|
-
if (soothingTimestamps.length > 0) {
|
|
110
|
-
console.log(` Soothing timestamps: ${soothingTimestamps.map(t => `+${t}ms`).join(', ')}`)
|
|
111
|
-
if (soothingTimestamps.length > 1) {
|
|
112
|
-
const intervals = soothingTimestamps.slice(1).map((t, i) => t - soothingTimestamps[i])
|
|
113
|
-
console.log(` Intervals between soothing: ${intervals.map(t => `${t}ms`).join(', ')}`)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (firstReplyAt) {
|
|
117
|
-
console.log(` First LLM reply at: +${firstReplyAt}ms`)
|
|
118
|
-
const lastSoothing = soothingTimestamps[soothingTimestamps.length - 1] || 0
|
|
119
|
-
if (lastSoothing && firstReplyAt > lastSoothing) {
|
|
120
|
-
console.log(` Gap (last soothing → first reply): ${firstReplyAt - lastSoothing}ms`)
|
|
121
|
-
}
|
|
122
|
-
console.log(` No soothing after first LLM reply: ${soothingTimestamps.every(t => t < firstReplyAt) ? '✅ YES' : '❌ NO'}`)
|
|
123
|
-
}
|
|
124
|
-
if (replyTimestamps.length > 0) {
|
|
125
|
-
console.log(` Total LLM reply chunks: ${replyTimestamps.length}`)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (mode === 'reminder') {
|
|
129
|
-
console.log(` ${gotNotification ? '✅' : 'ℹ️ '} Notification on event/post${gotNotification ? '' : ' (not received within timeout — may arrive later)'}`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (errors.length > 0) {
|
|
133
|
-
console.log('\nErrors:')
|
|
134
|
-
for (const e of errors) console.log(` ❌ ${e}`)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const allPassed = checks.every(([, ok]) => ok) && errors.length === 0
|
|
138
|
-
console.log(`\n${allPassed ? '✅ ALL CHECKS PASSED' : '❌ SOME CHECKS FAILED'}`)
|
|
139
|
-
return allPassed
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function done() {
|
|
143
|
-
const passed = printSummary()
|
|
144
|
-
client.end()
|
|
145
|
-
process.exit(passed ? 0 : 1)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ── MQTT ──
|
|
149
|
-
const MQTT_USERNAME = process.env.FOLOTOY_TEST_MQTT_USERNAME || SN
|
|
150
|
-
const MQTT_PASSWORD = process.env.FOLOTOY_TEST_MQTT_PASSWORD || KEY
|
|
151
|
-
|
|
152
|
-
const client = mqtt.connect(`mqtt://${HOST}:${PORT}`, {
|
|
153
|
-
clientId: `test-toy-${Date.now()}`,
|
|
154
|
-
username: MQTT_USERNAME,
|
|
155
|
-
password: MQTT_PASSWORD,
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
client.on('connect', () => {
|
|
159
|
-
console.log(`[connected] ${HOST}:${PORT} as ${SN}`)
|
|
160
|
-
console.log(`[mode] ${mode}`)
|
|
161
|
-
console.log(`[message] ${message}`)
|
|
162
|
-
|
|
163
|
-
client.subscribe([outboundTopic, notificationTopic], (err) => {
|
|
164
|
-
if (err) {
|
|
165
|
-
console.error('[subscribe error]', err.message)
|
|
166
|
-
process.exit(1)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const msg = {
|
|
170
|
-
msgId: 1,
|
|
171
|
-
identifier: 'chat_input',
|
|
172
|
-
inputParams: { text: message, recording_id: 999 },
|
|
173
|
-
}
|
|
174
|
-
console.log(`\n[publish] ${inboundTopic}`)
|
|
175
|
-
console.log(JSON.stringify(msg, null, 2))
|
|
176
|
-
client.publish(inboundTopic, JSON.stringify(msg))
|
|
177
|
-
})
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
client.on('message', (topic, payload) => {
|
|
181
|
-
let msg
|
|
182
|
-
try { msg = JSON.parse(payload.toString()) } catch { return }
|
|
183
|
-
|
|
184
|
-
if (topic === outboundTopic) {
|
|
185
|
-
const p = msg.outParams
|
|
186
|
-
const elapsed = Date.now() - startTime
|
|
187
|
-
const label = p?.is_finished ? 'FINISH' : `REPLY(order=${p?.order})`
|
|
188
|
-
console.log(`\n[OUTBOUND ${label}] +${elapsed}ms | ${p?.content || '(empty)'}`)
|
|
189
|
-
validateOutboundMessage(msg)
|
|
190
|
-
|
|
191
|
-
if (!p?.is_finished && p?.content) {
|
|
192
|
-
// Detect soothing vs LLM reply: soothing messages arrive before LLM starts
|
|
193
|
-
if (!firstReplyAt) {
|
|
194
|
-
// All messages before the first LLM sentence are soothing
|
|
195
|
-
// We'll determine this retrospectively, for now track all
|
|
196
|
-
}
|
|
197
|
-
// Heuristic: soothing messages are short Chinese phrases ending with ... or ~
|
|
198
|
-
const isSoothing = /[.…~]$/.test(p.content) && p.content.length < 30
|
|
199
|
-
if (isSoothing) {
|
|
200
|
-
soothingCount++
|
|
201
|
-
soothingTimestamps.push(elapsed)
|
|
202
|
-
gotSoothing = true
|
|
203
|
-
} else {
|
|
204
|
-
if (!firstReplyAt) firstReplyAt = elapsed
|
|
205
|
-
replyTimestamps.push(elapsed)
|
|
206
|
-
gotReply = true
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (p?.is_finished) {
|
|
210
|
-
gotFinish = true
|
|
211
|
-
if (mode === 'chat') {
|
|
212
|
-
setTimeout(done, 500)
|
|
213
|
-
} else {
|
|
214
|
-
console.log('\n[waiting up to 90s for notification on event/post topic...]')
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (topic === notificationTopic) {
|
|
220
|
-
console.log(`\n[NOTIFICATION] ${msg.outParams?.text || JSON.stringify(msg)}`)
|
|
221
|
-
validateNotificationMessage(msg)
|
|
222
|
-
gotNotification = true
|
|
223
|
-
setTimeout(done, 500)
|
|
224
|
-
}
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
client.on('error', (err) => {
|
|
228
|
-
console.error('[mqtt error]', err.message)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
// Timeout: chat 30s, reminder 120s
|
|
232
|
-
const timeoutMs = mode === 'reminder' ? 120_000 : 30_000
|
|
233
|
-
setTimeout(done, timeoutMs)
|
package/src/cli/install.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
2
|
-
import qrcode from 'qrcode-terminal'
|
|
3
|
-
import { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT } from '../config.js'
|
|
4
|
-
|
|
5
|
-
const PAIR_API_BASE = process.env.PAIR_API_BASE ?? 'https://pair.folotoy.cn'
|
|
6
|
-
const POLL_INTERVAL_MS = 3000
|
|
7
|
-
const POLL_TIMEOUT_MS = 300_000 // 5 minutes
|
|
8
|
-
|
|
9
|
-
// ── Types ──────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
type CreateSessionResponse = {
|
|
12
|
-
session_id: string
|
|
13
|
-
pair_url: string
|
|
14
|
-
expires_at: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type PollResponse =
|
|
18
|
-
| { status: 'pending' }
|
|
19
|
-
| { status: 'completed'; toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number }
|
|
20
|
-
| { status: 'expired' }
|
|
21
|
-
|
|
22
|
-
// ── Helpers ────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
function checkOpenClaw(): void {
|
|
25
|
-
try {
|
|
26
|
-
execSync('openclaw --version', { stdio: 'pipe' })
|
|
27
|
-
} catch {
|
|
28
|
-
console.error('Error: openclaw is not installed or not in PATH.')
|
|
29
|
-
console.error('Install it first: npm i -g openclaw')
|
|
30
|
-
process.exit(1)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function installPlugin(): void {
|
|
35
|
-
try {
|
|
36
|
-
const list = execSync('openclaw plugins list', { stdio: 'pipe' }).toString()
|
|
37
|
-
if (list.includes('folotoy-openclaw-plugin')) {
|
|
38
|
-
return // already installed
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
// ignore
|
|
42
|
-
}
|
|
43
|
-
console.log('Installing FoloToy plugin...')
|
|
44
|
-
execSync('openclaw plugins install @folotoy/folotoy-openclaw-plugin', { stdio: 'inherit' })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function createSession(): Promise<CreateSessionResponse> {
|
|
48
|
-
const res = await fetch(`${PAIR_API_BASE}/api/pair`, { method: 'POST' })
|
|
49
|
-
if (!res.ok) throw new Error(`Failed to create pairing session (HTTP ${res.status})`)
|
|
50
|
-
return res.json() as Promise<CreateSessionResponse>
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function displayQR(url: string): void {
|
|
54
|
-
qrcode.generate(url, { small: true }, (qr: string) => {
|
|
55
|
-
console.log(qr)
|
|
56
|
-
})
|
|
57
|
-
console.log(`Or open this URL on your phone: ${url}\n`)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function sleep(ms: number): Promise<void> {
|
|
61
|
-
return new Promise((r) => setTimeout(r, ms))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function pollSession(sessionId: string): Promise<PollResponse & { status: 'completed' }> {
|
|
65
|
-
const deadline = Date.now() + POLL_TIMEOUT_MS
|
|
66
|
-
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
67
|
-
let i = 0
|
|
68
|
-
|
|
69
|
-
while (Date.now() < deadline) {
|
|
70
|
-
process.stdout.write(`\r${frames[i++ % frames.length]} Waiting for pairing...`)
|
|
71
|
-
|
|
72
|
-
const res = await fetch(`${PAIR_API_BASE}/api/pair/${sessionId}`)
|
|
73
|
-
if (!res.ok) throw new Error(`Poll failed (HTTP ${res.status})`)
|
|
74
|
-
const data = (await res.json()) as PollResponse
|
|
75
|
-
|
|
76
|
-
if (data.status === 'completed') {
|
|
77
|
-
process.stdout.write('\r\x1b[32m✓\x1b[0m Paired successfully! \n')
|
|
78
|
-
return data as PollResponse & { status: 'completed' }
|
|
79
|
-
}
|
|
80
|
-
if (data.status === 'expired') {
|
|
81
|
-
process.stdout.write('\r')
|
|
82
|
-
throw new Error('Pairing session expired. Please try again.')
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
await sleep(POLL_INTERVAL_MS)
|
|
86
|
-
}
|
|
87
|
-
throw new Error('Pairing timed out after 5 minutes.')
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function restartGateway(): void {
|
|
91
|
-
try {
|
|
92
|
-
execSync('openclaw gateway restart', { stdio: 'inherit' })
|
|
93
|
-
} catch {
|
|
94
|
-
console.warn('⚠ Failed to restart gateway. You can restart manually: openclaw gateway restart')
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function writeConfig(result: { toy_sn: string; toy_key: string; mqtt_host?: string; mqtt_port?: number }): void {
|
|
99
|
-
execSync(`openclaw config set channels.folotoy.flow direct`, { stdio: 'pipe' })
|
|
100
|
-
execSync(`openclaw config set channels.folotoy.toy_sn ${result.toy_sn}`, { stdio: 'pipe' })
|
|
101
|
-
execSync(`openclaw config set channels.folotoy.toy_key ${result.toy_key}`, { stdio: 'pipe' })
|
|
102
|
-
|
|
103
|
-
const mqttHost = result.mqtt_host ?? DEFAULT_MQTT_HOST
|
|
104
|
-
const mqttPort = result.mqtt_port ?? DEFAULT_MQTT_PORT
|
|
105
|
-
execSync(`openclaw config set channels.folotoy.mqtt_host ${mqttHost}`, { stdio: 'pipe' })
|
|
106
|
-
execSync(`openclaw config set channels.folotoy.mqtt_port ${mqttPort}`, { stdio: 'pipe' })
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── Main ───────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
async function main() {
|
|
112
|
-
const command = process.argv[2]
|
|
113
|
-
|
|
114
|
-
if (command !== 'install') {
|
|
115
|
-
console.log('Usage: npx @folotoy/folotoy-openclaw-plugin install')
|
|
116
|
-
process.exit(command ? 1 : 0)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
console.log('🧸 FoloToy OpenClaw Plugin Installer\n')
|
|
120
|
-
|
|
121
|
-
// Step 1: check prerequisites
|
|
122
|
-
console.log('Checking openclaw...')
|
|
123
|
-
checkOpenClaw()
|
|
124
|
-
console.log('✓ openclaw found\n')
|
|
125
|
-
|
|
126
|
-
// Step 2: install plugin if not present
|
|
127
|
-
installPlugin()
|
|
128
|
-
|
|
129
|
-
// Step 3: create pairing session
|
|
130
|
-
console.log('Creating pairing session...\n')
|
|
131
|
-
const session = await createSession()
|
|
132
|
-
|
|
133
|
-
// Step 4: display QR code
|
|
134
|
-
console.log('Scan this QR code with your phone,')
|
|
135
|
-
console.log('then scan your toy\'s QR code on the phone:\n')
|
|
136
|
-
displayQR(session.pair_url)
|
|
137
|
-
|
|
138
|
-
// Step 5: poll for result
|
|
139
|
-
const result = await pollSession(session.session_id)
|
|
140
|
-
|
|
141
|
-
// Step 6: write config
|
|
142
|
-
console.log('\nWriting configuration...')
|
|
143
|
-
writeConfig(result)
|
|
144
|
-
|
|
145
|
-
// Step 7: restart gateway
|
|
146
|
-
console.log('\nRestarting gateway...')
|
|
147
|
-
restartGateway()
|
|
148
|
-
|
|
149
|
-
// Step 8: done
|
|
150
|
-
console.log('\n\x1b[32m✓ FoloToy plugin installed and configured!\x1b[0m')
|
|
151
|
-
console.log(` Toy SN: ${result.toy_sn}`)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
main().catch((err) => {
|
|
155
|
-
console.error(`\n\x1b[31mError:\x1b[0m ${err instanceof Error ? err.message : String(err)}`)
|
|
156
|
-
process.exit(1)
|
|
157
|
-
})
|