@mahe_pkm/buzl-capi 0.1.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.
@@ -0,0 +1,388 @@
1
+ /**
2
+ * =========================================================================
3
+ * BUZL CAPI & MULTI-CHANNEL TRACKING DISPATCHER — TERMINAL WIZARD
4
+ * Official Buzl Brand Interface (Electric Blue #0040FF & Energy Amber #F59E0B)
5
+ * =========================================================================
6
+ */
7
+ const readline = require('readline');
8
+ const path = require('path');
9
+ const { scanProject } = require('../core/scanner');
10
+ const { applyInjection, removeTracking, removeService } = require('../core/injector');
11
+ const { runVerification, testIndividualForm } = require('../core/tester');
12
+ const { listBackups, restoreBackup, manualBackup } = require('../core/rollback');
13
+ const { startGuiServer } = require('../gui/server');
14
+
15
+ // Official Buzl Brand Color Tokens (with ANSI Truecolor & fallback)
16
+ const colors = {
17
+ blue: (s) => `\x1b[38;2;59;95;245m${s}\x1b[0m`, // #3B5FF5 Electric Brand Blue
18
+ blueBold: (s) => `\x1b[1m\x1b[38;2;59;95;245m${s}\x1b[0m`,
19
+ amber: (s) => `\x1b[38;2;245;158;11m${s}\x1b[0m`, // #F59E0B Energy Amber
20
+ amberBold: (s) => `\x1b[1m\x1b[38;2;245;158;11m${s}\x1b[0m`,
21
+ green: (s) => `\x1b[38;2;16;185;129m${s}\x1b[0m`, // #10B981 Emerald Green
22
+ greenBold: (s) => `\x1b[1m\x1b[38;2;16;185;129m${s}\x1b[0m`,
23
+ red: (s) => `\x1b[38;2;239;68;68m${s}\x1b[0m`, // #EF4444 Crimson Red
24
+ redBold: (s) => `\x1b[1m\x1b[38;2;239;68;68m${s}\x1b[0m`,
25
+ ice: (s) => `\x1b[38;2;147;173;253m${s}\x1b[0m`, // #93ADFD Ice Blue
26
+ cyan: (s) => `\x1b[38;2;56;189;248m${s}\x1b[0m`, // #38BDF8 Sky Cyan
27
+ slate: (s) => `\x1b[38;2;148;163;184m${s}\x1b[0m`, // #94A3B8 Secondary Text
28
+ dim: (s) => `\x1b[38;2;100;116;139m${s}\x1b[0m`, // #64748B Muted Slate
29
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
30
+ border: (s) => `\x1b[38;2;59;95;245m${s}\x1b[0m`
31
+ };
32
+
33
+ function askQuestion(rl, query) {
34
+ return new Promise((resolve) => rl.question(query, resolve));
35
+ }
36
+
37
+ /**
38
+ * Render the full intuitive Buzl CAPI dashboard in the terminal
39
+ */
40
+ function printLiveState(scan) {
41
+ const live = scan.liveState || {};
42
+ const b = colors.border;
43
+
44
+ // 1. Hero Banner in CODE Typography
45
+ console.log('');
46
+ console.log(colors.blueBold(' ██████╗ ██╗ ██╗███████╗██╗ ') + colors.amberBold(' ██████╗ █████╗ ██████╗ ██╗'));
47
+ console.log(colors.blueBold(' ██╔══██╗██║ ██║╚══███╔╝██║ ') + colors.amberBold(' ██╔════╝██╔══██╗██╔══██╗██║'));
48
+ console.log(colors.blueBold(' ██████╔╝██║ ██║ ███╔╝ ██║ ███') + colors.amberBold('██╗██║ ███████║██████╔╝██║'));
49
+ console.log(colors.blueBold(' ██╔══██╗██║ ██║ ███╔╝ ██║ ╚══') + colors.amberBold('══╝██║ ██╔══██║██╔═══╝ ██║'));
50
+ console.log(colors.blueBold(' ██████╔╝╚██████╔╝███████╗███████╗') + colors.amberBold(' ╚██████╗██║ ██║██║ ██║'));
51
+ console.log(colors.blueBold(' ╚═════╝ ╚═════╝ ╚══════╝╚══════╝') + colors.amberBold(' ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝'));
52
+ console.log(colors.ice(' ⚡ BUZL CONVERSIONS API & MULTI-CHANNEL DISPATCHER • v1.0.0\n'));
53
+
54
+ // 2. Target Project Scope Card
55
+ console.log(b(' ╭─────────────────────────── [ 📂 TARGET PROJECT ] ───────────────────────────╮'));
56
+ console.log(b(' │') + ' Path : ' + colors.bold(scan.rootDir));
57
+ console.log(b(' │') + ' Pages : ' + colors.ice(`${scan.totalHtmlFiles} HTML file(s) discovered`));
58
+ console.log(b(' │') + ' Location : ' + colors.amber(scan.detectedLocation || 'N/A'));
59
+ console.log(b(' ╰─────────────────────────────────────────────────────────────────────────────╯'));
60
+ console.log('');
61
+
62
+ // 3. Live Service & Dispatch Matrix Card
63
+ console.log(b(' ╭──────────────────────── [ ⚡ LIVE SERVICE STATUS ] ─────────────────────────╮'));
64
+ const gtmId = (live.gtm && live.gtm.id) || null;
65
+ const metaId = (live.meta && live.meta.id) || null;
66
+ const capiActive = !!(live.buzlCapi && live.buzlCapi.active);
67
+ const sheetsActive = !!(live.googleSheets && live.googleSheets.active);
68
+ const zohoActive = !!(live.zoho && live.zoho.active);
69
+ const waActive = !!(live.whatsapp && live.whatsapp.active);
70
+ const backupsCount = (live.backups && live.backups.length) || 0;
71
+
72
+ const detectedWa = scan.detectedWhatsapp || (live.whatsapp && live.whatsapp.detectedNumber);
73
+ let waDetail = 'Not configured';
74
+ if (waActive) {
75
+ waDetail = `Active (${live.whatsapp.number})`;
76
+ } else if (detectedWa) {
77
+ waDetail = `Auto-detected from buttons: ${detectedWa}`;
78
+ }
79
+
80
+ const rows = [
81
+ { name: '🏷️ GTM Container', active: !!(live.gtm && live.gtm.active), detail: gtmId ? `Container: ${gtmId}` : 'Not injected' },
82
+ { name: '🎯 Meta Pixel', active: !!(live.meta && live.meta.active), detail: metaId ? `Pixel ID: ${metaId}` : 'Not injected' },
83
+ { name: '⚡ Buzl CAPI', active: capiActive, detail: capiActive ? 'Connected (https://dev.api.gobuzl.com/api/leads)' : 'Inactive (Ready to activate)' },
84
+ { name: '📊 Google Sheets', active: sheetsActive, detail: sheetsActive ? (live.googleSheets.url.slice(0, 42) + '...') : 'Not connected' },
85
+ { name: '💼 Zoho CRM Web', active: zohoActive, detail: zohoActive ? 'Connected (crm.zoho.in)' : 'Not connected' },
86
+ { name: '💬 WhatsApp Link', active: waActive, detail: waDetail },
87
+ { name: '💾 Safe Snapshots', active: backupsCount > 0, detail: `${backupsCount} snapshot(s) on disk (1-click restore)` }
88
+ ];
89
+
90
+ rows.forEach(r => {
91
+ const statusText = r.active ? colors.greenBold('● ACTIVE ') : colors.dim('○ INACTIVE ');
92
+ const detailColor = r.active ? colors.ice : (r.name.includes('WhatsApp') && detectedWa ? colors.amber : colors.dim);
93
+ console.log(b(' │') + ` ${r.name.padEnd(20)} ${statusText} ${detailColor(r.detail)}`);
94
+ });
95
+ console.log(b(' ╰─────────────────────────────────────────────────────────────────────────────╯'));
96
+ console.log('');
97
+
98
+ // 4. Discovered Forms Inventory Card
99
+ if (scan.forms && scan.forms.length > 0) {
100
+ console.log(b(' ╭────────────────────────── [ 📝 DETECTED FORMS ] ───────────────────────────╮'));
101
+ scan.forms.forEach((f, idx) => {
102
+ const inputsList = f.inputs.map(i => i.name).join(', ') || 'No named inputs';
103
+ const waTag = f.detectedWhatsapp ? colors.green(` [💬 WA: ${f.detectedWhatsapp}]`) : '';
104
+ console.log(b(' │') + ` ${colors.amberBold('[' + (idx + 1) + ']')} ${colors.cyan(f.selector)} in ${colors.dim(f.file)}${waTag} (${f.inputCount} inputs: ${colors.slate(inputsList)})`);
105
+ });
106
+ console.log(b(' ╰─────────────────────────────────────────────────────────────────────────────╯'));
107
+ console.log('');
108
+ }
109
+
110
+ // 5. Action Command Center Menu
111
+ console.log(colors.amberBold(' ⚡ ACTION COMMAND CENTER:'));
112
+ console.log(` ${colors.amberBold('[1]')} ${colors.blue('⚡ Deploy / Update Tracking Configuration')}`);
113
+ console.log(` ${colors.amberBold('[2]')} ${colors.green('🧪 Test Individual Form Submission (Interactive)')}`);
114
+ console.log(` ${colors.amberBold('[3]')} ${colors.ice('💾 Create Named Snapshot Backup')}`);
115
+ console.log(` ${colors.amberBold('[4]')} ${colors.ice('↺ View & 1-Click Restore Backups')}`);
116
+ console.log(` ${colors.amberBold('[5]')} ${colors.red('🧹 Selective Service Removal / Nuclear Clean Uninstall')}`);
117
+ console.log(` ${colors.amberBold('[6]')} ${colors.cyan('🌐 Launch Web GUI Dashboard on http://localhost:3333')}`);
118
+ console.log(` ${colors.dim('[0] ✖ Exit Terminal')}\n`);
119
+ }
120
+
121
+ async function runTerminalWizard(rootDir) {
122
+ const rl = readline.createInterface({
123
+ input: process.stdin,
124
+ output: process.stdout
125
+ });
126
+
127
+ try {
128
+ const scan = scanProject(rootDir);
129
+ printLiveState(scan);
130
+
131
+ if (scan.totalHtmlFiles === 0) {
132
+ console.log(colors.red(' ❌ No HTML files found in current directory.'));
133
+ rl.close();
134
+ return;
135
+ }
136
+
137
+ const choice = (await askQuestion(rl, colors.amberBold(' ⚡ Select an option [1-6, 0]: '))).trim();
138
+
139
+ if (choice === '0' || choice === '') {
140
+ console.log(colors.dim('\n Exiting Buzl Tracker. Have a productive day!\n'));
141
+ rl.close();
142
+ return;
143
+ }
144
+
145
+ // 1. Inject / Update Tracking
146
+ if (choice === '1') {
147
+ console.log('\n' + colors.blueBold(' ╭─ [ ⚡ CONFIGURE TRACKING & CREDENTIALS ] ─────────────────────────╮'));
148
+
149
+ const gtmPrompt = scan.liveState.gtm.id
150
+ ? `GTM Container ID [${scan.liveState.gtm.id}] (type 'none' to remove): `
151
+ : 'GTM Container ID (e.g. GTM-TRF9ZHHH, or Enter to skip): ';
152
+ let gtmId = (await askQuestion(rl, ` │ ${gtmPrompt}`)).trim();
153
+ if (['none', 'remove', '0', 'off', 'disable'].includes(gtmId.toLowerCase())) {
154
+ gtmId = '';
155
+ } else if (!gtmId && scan.liveState.gtm.id) {
156
+ gtmId = scan.liveState.gtm.id;
157
+ }
158
+
159
+ const metaPrompt = scan.liveState.meta.id
160
+ ? `Meta Pixel ID [${scan.liveState.meta.id}] (type 'none' to remove): `
161
+ : 'Meta Pixel ID (or Enter to skip): ';
162
+ let metaPixelId = (await askQuestion(rl, ` │ ${metaPrompt}`)).trim();
163
+ if (['none', 'remove', '0', 'off', 'disable'].includes(metaPixelId.toLowerCase())) {
164
+ metaPixelId = '';
165
+ } else if (!metaPixelId && scan.liveState.meta.id) {
166
+ metaPixelId = scan.liveState.meta.id;
167
+ }
168
+
169
+ const sheetPrompt = scan.liveState.googleSheets.url
170
+ ? `Google Sheet Web App URL [Keep existing] (type 'none' to remove): `
171
+ : 'Google Sheet Web App URL (or Enter to skip): ';
172
+ let googleSheetUrl = (await askQuestion(rl, ` │ ${sheetPrompt}`)).trim();
173
+ if (['none', 'remove', '0', 'off', 'disable'].includes(googleSheetUrl.toLowerCase())) {
174
+ googleSheetUrl = '';
175
+ } else if (!googleSheetUrl && scan.liveState.googleSheets.url) {
176
+ googleSheetUrl = scan.liveState.googleSheets.url;
177
+ }
178
+
179
+ const buzlCapiUser = (await askQuestion(rl, ` │ Buzl CAPI authUser [${scan.liveState.buzlCapi.authUser || '2BuzlmqpHJeVBow0dzR9gP3$uQLxIA'}]: `)).trim() || (scan.liveState.buzlCapi.authUser || '2BuzlmqpHJeVBow0dzR9gP3$uQLxIA');
180
+ const buzlCapiPass = (await askQuestion(rl, ` │ Buzl CAPI authPass: `)).trim() || 'dgAY%nH1MNPgOvGaYRg6ynomM3mbJgGjr%Z3FcPCJNzvm#KjV!I%Y9tf$bDacBgPIABuzl';
181
+
182
+ const detectedWa = scan.detectedWhatsapp || (scan.liveState.whatsapp && scan.liveState.whatsapp.detectedNumber) || '';
183
+ const existingWa = scan.liveState.whatsapp && scan.liveState.whatsapp.number;
184
+ const defaultWa = existingWa || detectedWa || '';
185
+ const waPrompt = defaultWa
186
+ ? `WhatsApp Business Number [${defaultWa}] (type 'none' to remove): `
187
+ : 'WhatsApp Business Number (or Enter to skip): ';
188
+ let whatsappNumber = (await askQuestion(rl, ` │ ${waPrompt}`)).trim();
189
+ if (['none', 'remove', '0', 'off', 'disable'].includes(whatsappNumber.toLowerCase())) {
190
+ whatsappNumber = '';
191
+ } else if (!whatsappNumber && defaultWa) {
192
+ whatsappNumber = defaultWa;
193
+ }
194
+ console.log(colors.blueBold(' ╰───────────────────────────────────────────────────────────────────╯'));
195
+
196
+ const config = {
197
+ gtmId,
198
+ metaPixelId,
199
+ googleSheetUrl,
200
+ whatsappNumber,
201
+ whatsapp: {
202
+ number: whatsappNumber
203
+ },
204
+ siteLocation: scan.detectedLocation,
205
+ buzlCapi: {
206
+ endpoint: 'https://dev.api.gobuzl.com/api/leads',
207
+ authUser: buzlCapiUser,
208
+ authPass: buzlCapiPass
209
+ }
210
+ };
211
+
212
+ console.log('\n' + colors.cyan(' 🚀 Applying injection and deploying runtime...'));
213
+ const htmlFilePaths = scan.files.map(f => f.filePath);
214
+ const injectResult = applyInjection(rootDir, htmlFilePaths, config);
215
+ console.log(colors.greenBold(` ✔ Successfully injected into ${injectResult.modifiedFiles.length} file(s).`));
216
+
217
+ console.log(colors.amber('\n 🧪 Running automated verification suite...'));
218
+ const testReport = await runVerification(rootDir, htmlFilePaths, config);
219
+ testReport.tests.forEach(t => {
220
+ const icon = t.passed ? colors.greenBold('✔ PASS') : colors.redBold('✖ FAIL');
221
+ console.log(` ${icon}: ${t.name} ${t.detail ? colors.dim(`(${t.detail})`) : ''}`);
222
+ });
223
+ console.log(colors.greenBold(`\n 🎉 Completed: ${testReport.passedCount}/${testReport.total} tests passed!\n`));
224
+ rl.close();
225
+ return;
226
+ }
227
+
228
+ // 2. Test Individual Form
229
+ if (choice === '2') {
230
+ if (!scan.forms || scan.forms.length === 0) {
231
+ console.log(colors.red(' ❌ No forms discovered on site to test.'));
232
+ rl.close();
233
+ return;
234
+ }
235
+ console.log('\n' + colors.amberBold(' Select Form to Test:'));
236
+ scan.forms.forEach((f, idx) => {
237
+ console.log(` ${colors.amberBold('[' + (idx + 1) + ']')} ${colors.cyan(f.selector)} in ${colors.dim(f.file)} (${f.inputCount} inputs)`);
238
+ });
239
+ const formChoice = parseInt((await askQuestion(rl, colors.amberBold(`\n Choose form [1-${scan.forms.length}]: `))).trim(), 10);
240
+ const selectedForm = scan.forms[formChoice - 1];
241
+ if (!selectedForm) {
242
+ console.log(colors.red(' ❌ Invalid form selection.'));
243
+ rl.close();
244
+ return;
245
+ }
246
+
247
+ console.log(`\n Testing Form: ${colors.cyan(selectedForm.selector || selectedForm.formId || 'Form')}`);
248
+ const defaultPhone = selectedForm.detectedWhatsapp || scan.detectedWhatsapp || '';
249
+ const phonePrompt = defaultPhone ? ` Enter test phone [${defaultPhone}]: ` : ' Enter test phone: ';
250
+ const testPhone = (await askQuestion(rl, phonePrompt)).trim() || defaultPhone;
251
+ const testName = (await askQuestion(rl, ' Enter test name [Test Lead]: ')).trim() || 'Test Lead';
252
+
253
+ const formFields = { phone: testPhone, name: testName, formId: selectedForm.formId };
254
+ selectedForm.inputs.forEach(inp => {
255
+ if (!formFields[inp.name]) {
256
+ const lower = (inp.name || '').toLowerCase();
257
+ if (lower.includes('service') || lower.includes('inquiry') || lower.includes('treatment') || lower.includes('subject')) {
258
+ formFields[inp.name] = selectedForm.formId || 'General Inquiry';
259
+ } else if (lower.includes('email') || lower.includes('mail')) {
260
+ formFields[inp.name] = 'test-lead@example.com';
261
+ } else if (lower.includes('city') || lower.includes('loc')) {
262
+ formFields[inp.name] = scan.detectedLocation || '';
263
+ } else {
264
+ formFields[inp.name] = 'Test Value';
265
+ }
266
+ }
267
+ });
268
+
269
+ console.log(colors.amber('\n ⏳ Dispatching synthetic lead across active destinations...'));
270
+ const testResults = await testIndividualForm(rootDir, selectedForm.formId, formFields, scan.existingConfig || {});
271
+ console.log('\n' + colors.blueBold(' Test Results:'));
272
+ for (const [ch, res] of Object.entries(testResults.channels)) {
273
+ const icon = res.tested ? (res.ok ? colors.greenBold('✔') : colors.redBold('✖')) : colors.dim('○');
274
+ const statusLabel = res.tested ? (res.ok ? colors.greenBold('Delivered') : colors.redBold('Failed')) : colors.dim('Disabled');
275
+ console.log(` ${icon} ${colors.bold(ch.padEnd(14))}: [${statusLabel}] ${res.message}`);
276
+ }
277
+ console.log('');
278
+ rl.close();
279
+ return;
280
+ }
281
+
282
+ // 3. Create Named Backup
283
+ if (choice === '3') {
284
+ const bName = (await askQuestion(rl, colors.amberBold('\n Enter snapshot name (e.g. "Pre-Sprint Checkpoint"): '))).trim();
285
+ const bRes = manualBackup(rootDir, bName);
286
+ if (bRes.success) {
287
+ console.log(colors.greenBold(`\n ✔ ${bRes.message}\n`));
288
+ } else {
289
+ console.log(colors.redBold(`\n ✖ ${bRes.message}\n`));
290
+ }
291
+ rl.close();
292
+ return;
293
+ }
294
+
295
+ // 4. View & Restore Named Backups
296
+ if (choice === '4') {
297
+ const backups = listBackups(rootDir);
298
+ if (backups.length === 0) {
299
+ console.log(colors.amber('\n No backups found on disk.\n'));
300
+ rl.close();
301
+ return;
302
+ }
303
+ console.log('\n' + colors.amberBold(' Available Snapshots on Disk:'));
304
+ backups.forEach((b, idx) => {
305
+ console.log(` ${colors.amberBold('[' + (idx + 1) + ']')} ${colors.bold(b.name)} ${colors.dim('(' + b.dirName + ')')} — ${colors.ice(b.timestamp)} [${b.filesCount} files]`);
306
+ });
307
+ const rChoice = (await askQuestion(rl, colors.amberBold(`\n Choose backup to restore [1-${backups.length}, or Enter to cancel]: `))).trim();
308
+ const targetBackup = backups[parseInt(rChoice, 10) - 1];
309
+ if (targetBackup) {
310
+ const confirm = (await askQuestion(rl, colors.red(` Restore "${targetBackup.name}"? This replaces current HTML files [y/N]: `))).trim().toLowerCase();
311
+ if (confirm === 'y' || confirm === 'yes') {
312
+ const res = restoreBackup(rootDir, targetBackup.dirName);
313
+ console.log(colors.greenBold(`\n ✔ ${res.message}\n`));
314
+ } else {
315
+ console.log(colors.dim('\n Restore cancelled.\n'));
316
+ }
317
+ }
318
+ rl.close();
319
+ return;
320
+ }
321
+
322
+ // 5. Selective Service Removal & Clean Uninstall
323
+ if (choice === '5') {
324
+ console.log('\n' + colors.amberBold(' ╭─ [ 🧹 SELECTIVE SERVICE REMOVAL & UNINSTALL ] ────────────────────╮'));
325
+ console.log(` │ ${colors.amberBold('[1]')} 🏷️ Remove Google Tag Manager (GTM) Container Only │`);
326
+ console.log(` │ ${colors.amberBold('[2]')} 🎯 Remove Meta Pixel & Client CAPI Only │`);
327
+ console.log(` │ ${colors.amberBold('[3]')} ⚡ Remove Buzl CAPI Server-Side Endpoint Only │`);
328
+ console.log(` │ ${colors.amberBold('[4]')} 📊 Remove Google Sheets Direct Sync Only │`);
329
+ console.log(` │ ${colors.amberBold('[5]')} 💼 Remove Zoho CRM Web-to-Lead Only │`);
330
+ console.log(` │ ${colors.amberBold('[6]')} 🧹 Cleanly Remove All Tracking (Nuclear Uninstall) │`);
331
+ console.log(` │ ${colors.dim('[0]')} ↩ Cancel / Back to Menu │`);
332
+ console.log(colors.amberBold(' ╰───────────────────────────────────────────────────────────────────╯'));
333
+
334
+ const remChoice = (await askQuestion(rl, colors.amberBold('\n Select service to remove [1-6, 0]: '))).trim();
335
+ if (remChoice === '0' || remChoice === '') {
336
+ console.log(colors.dim('\n Removal cancelled.\n'));
337
+ rl.close();
338
+ return;
339
+ }
340
+
341
+ const serviceMap = {
342
+ '1': 'gtm',
343
+ '2': 'meta',
344
+ '3': 'capi',
345
+ '4': 'sheets',
346
+ '5': 'zoho',
347
+ '6': 'all'
348
+ };
349
+
350
+ const targetService = serviceMap[remChoice];
351
+ if (!targetService) {
352
+ console.log(colors.red(' ❌ Invalid selection.'));
353
+ rl.close();
354
+ return;
355
+ }
356
+
357
+ const confirm = (await askQuestion(rl, colors.red(`\n Confirm removal of ${targetService.toUpperCase()}? (Safety backup will be saved) [y/N]: `))).trim().toLowerCase();
358
+ if (confirm === 'y' || confirm === 'yes') {
359
+ const res = removeService(rootDir, targetService);
360
+ if (res.success) {
361
+ console.log(colors.greenBold(`\n ✔ ${res.message}\n`));
362
+ } else {
363
+ console.log(colors.redBold(`\n ✖ ${res.message}\n`));
364
+ }
365
+ } else {
366
+ console.log(colors.dim('\n Removal cancelled.\n'));
367
+ }
368
+ rl.close();
369
+ return;
370
+ }
371
+
372
+ // 6. Launch Web GUI
373
+ if (choice === '6') {
374
+ rl.close();
375
+ startGuiServer(rootDir);
376
+ return;
377
+ }
378
+
379
+ } catch (err) {
380
+ console.error(colors.redBold('\n Error: ' + err.message + '\n'));
381
+ rl.close();
382
+ }
383
+ }
384
+
385
+ module.exports = {
386
+ runTerminalWizard,
387
+ printLiveState
388
+ };