@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.
- package/Buzl_GoogleAppsScript_Template.gs +948 -0
- package/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/assets/js/buzl-tracking.js +390 -0
- package/bin/cli.js +155 -0
- package/package.json +54 -0
- package/src/cli/terminal.js +388 -0
- package/src/core/injector.js +321 -0
- package/src/core/rollback.js +281 -0
- package/src/core/scanner.js +506 -0
- package/src/core/tester.js +357 -0
- package/src/gui/public/app.js +942 -0
- package/src/gui/public/index.html +634 -0
- package/src/gui/public/style.css +1130 -0
- package/src/gui/server.js +232 -0
- package/src/index.js +47 -0
- package/src/templates/GoogleAppsScript.gs +948 -0
- package/src/templates/buzl-tracking.js +475 -0
- package/src/templates/gtm-meta-snippets.js +149 -0
|
@@ -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
|
+
};
|