@mahe_pkm/buzl-capi 0.1.2 → 0.1.3

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.
@@ -1,321 +1,321 @@
1
- /**
2
- * Safe HTML & Tracking Injector
3
- * Injects GTM, Meta Pixel, Form Sync runtime, and configuration
4
- */
5
- const fs = require('fs');
6
- const path = require('path');
7
- const {
8
- HEAD_START_MARKER,
9
- HEAD_END_MARKER,
10
- BODY_START_MARKER,
11
- BODY_END_MARKER,
12
- generateHeadSnippet,
13
- generateBodySnippet
14
- } = require('../templates/gtm-meta-snippets');
15
- const { createBackup } = require('./rollback');
16
-
17
- const RUNTIME_START_MARKER = '<!-- BUZL_TRACKING_RUNTIME_START -->';
18
- const RUNTIME_END_MARKER = '<!-- BUZL_TRACKING_RUNTIME_END -->';
19
-
20
- /**
21
- * Generate runtime script tag & inline configuration
22
- */
23
- function generateRuntimeSnippet(config, scriptRelPath) {
24
- const zohoObj = config.zoho || {};
25
- const zohoXn = (config.zohoXnqsjsdp || zohoObj.xnQsjsdp || '').trim();
26
- const zohoEndpoint = zohoXn ? (config.zohoEndpoint || zohoObj.endpoint || 'https://crm.zoho.in/crm/WebToLeadForm') : '';
27
- const zohoXm = zohoXn ? (config.zohoXmiwtld || zohoObj.xmIwtLD || '') : '';
28
- const zohoFields = zohoXn ? (config.zohoFields || zohoObj.fields || { lastName: 'Last Name', phone: 'Mobile', location: 'City' }) : {};
29
-
30
- const clientConfig = {
31
- enableGTM: !!(config.gtmId && config.gtmId.trim()),
32
- gtmEvent: config.gtmEvent || 'lead_form_submitted',
33
- enableMeta: !!(config.metaPixelId && config.metaPixelId.trim()),
34
- trackMetaLeadEvent: !!config.trackMetaLeadEvent,
35
- googleSheetUrl: config.googleSheetUrl || '',
36
- siteLocation: config.siteLocation || '',
37
- zoho: {
38
- endpoint: zohoEndpoint,
39
- xnQsjsdp: zohoXn,
40
- xmIwtLD: zohoXm,
41
- fields: zohoFields
42
- },
43
- buzlCapi: {
44
- endpoint: (config.buzlCapi && config.buzlCapi.endpoint) || config.buzlCapiEndpoint || '',
45
- authUser: (config.buzlCapi && config.buzlCapi.authUser) || config.buzlCapiUser || '',
46
- authPass: (config.buzlCapi && config.buzlCapi.authPass) || config.buzlCapiPass || ''
47
- },
48
- whatsapp: {
49
- number: config.whatsappNumber || (config.whatsapp && config.whatsapp.number) || '',
50
- template: config.whatsappTemplate || (config.whatsapp && config.whatsapp.template) || 'Hi, I submitted an inquiry from {name} in {location}.'
51
- },
52
- safetyTimeoutMs: config.safetyTimeoutMs || 800
53
- };
54
-
55
- return [
56
- RUNTIME_START_MARKER,
57
- ' <script id="buzl-tracking-config">',
58
- ` window.__BUZL_CONFIG__ = ${JSON.stringify(clientConfig, null, 2)};`,
59
- ' </script>',
60
- ` <script src="${scriptRelPath}" defer></script>`,
61
- RUNTIME_END_MARKER
62
- ].join('\n');
63
- }
64
-
65
- /**
66
- * Injects tracking into a single HTML string
67
- */
68
- function injectHtml(htmlContent, config, scriptRelPath = 'assets/js/buzl-tracking.js') {
69
- let content = htmlContent;
70
-
71
- const hasHeadTrackers = !!((config.gtmId && config.gtmId.trim()) || (config.metaPixelId && config.metaPixelId.trim()));
72
- const headSnippet = hasHeadTrackers ? generateHeadSnippet(config) : '';
73
- const bodySnippet = hasHeadTrackers ? generateBodySnippet(config) : '';
74
- const runtimeSnippet = generateRuntimeSnippet(config, scriptRelPath);
75
-
76
- // 1. Head snippet replacement or injection
77
- const headMarkerRegex = new RegExp(`[\\t ]*${HEAD_START_MARKER}[\\s\\S]*?${HEAD_END_MARKER}\\r?\\n?`, 'g');
78
- if (headMarkerRegex.test(content)) {
79
- content = content.replace(headMarkerRegex, hasHeadTrackers ? `${headSnippet}\n` : '');
80
- } else if (hasHeadTrackers) {
81
- if (/<\/head>/i.test(content)) {
82
- content = content.replace(/<\/head>/i, `${headSnippet}\n</head>`);
83
- } else if (/<head[\s>]/i.test(content)) {
84
- content = content.replace(/(<head[\s>][^>]*>)/i, `$1\n${headSnippet}`);
85
- }
86
- }
87
-
88
- // 2. Body noscript snippet replacement or injection
89
- const bodyMarkerRegex = new RegExp(`[\\t ]*${BODY_START_MARKER}[\\s\\S]*?${BODY_END_MARKER}\\r?\\n?`, 'g');
90
- if (bodyMarkerRegex.test(content)) {
91
- content = content.replace(bodyMarkerRegex, hasHeadTrackers ? `${bodySnippet}\n` : '');
92
- } else if (hasHeadTrackers) {
93
- if (/<body[\s>]/i.test(content)) {
94
- content = content.replace(/(<body[\s>][^>]*>)/i, `$1\n${bodySnippet}`);
95
- }
96
- }
97
-
98
- // 3. Runtime script replacement or injection
99
- const runtimeMarkerRegex = new RegExp(`${RUNTIME_START_MARKER}[\\s\\S]*?${RUNTIME_END_MARKER}`, 'g');
100
- if (runtimeMarkerRegex.test(content)) {
101
- content = content.replace(runtimeMarkerRegex, runtimeSnippet);
102
- } else if (/<\/body>/i.test(content)) {
103
- content = content.replace(/<\/body>/i, `${runtimeSnippet}\n</body>`);
104
- } else {
105
- content += `\n${runtimeSnippet}`;
106
- }
107
-
108
- // 4. Form tag attribute injection (adds data-buzl-track="true" to forms if missing)
109
- content = content.replace(/<form\b(?![^>]*\bdata-buzl-)([^>]*)>/gi, (match, p1) => {
110
- return `<form data-buzl-track="true"${p1}>`;
111
- });
112
-
113
- return content;
114
- }
115
-
116
- /**
117
- * Apply injection across the whole project
118
- */
119
- function applyInjection(rootDir, htmlFiles, config) {
120
- // If whatsapp number is not explicitly configured, fallback to auto-detected number from buttons
121
- if (!config.whatsappNumber && !(config.whatsapp && config.whatsapp.number)) {
122
- try {
123
- const { scanProject } = require('./scanner');
124
- const scan = scanProject(rootDir);
125
- if (scan && scan.detectedWhatsapp) {
126
- config = Object.assign({}, config, {
127
- whatsappNumber: scan.detectedWhatsapp,
128
- whatsapp: Object.assign({}, config.whatsapp || {}, { number: scan.detectedWhatsapp, autoDetected: true })
129
- });
130
- }
131
- } catch (e) {}
132
- }
133
-
134
- // 1. Ensure backup first
135
- const backup = createBackup(htmlFiles, rootDir);
136
-
137
- // 2. Deploy runtime SDK script into assets/js/
138
- const targetJsDir = path.join(rootDir, 'assets', 'js');
139
- fs.mkdirSync(targetJsDir, { recursive: true });
140
-
141
- const sdkSourcePath = path.join(__dirname, '..', 'templates', 'buzl-tracking.js');
142
- const sdkDestPath = path.join(targetJsDir, 'buzl-tracking.js');
143
- fs.copyFileSync(sdkSourcePath, sdkDestPath);
144
-
145
- // 3. Deploy GoogleAppsScript.gs to root or reference folder
146
- const gsSource = path.join(__dirname, '..', 'templates', 'GoogleAppsScript.gs');
147
- const gsDest = path.join(rootDir, 'Buzl_GoogleAppsScript_Template.gs');
148
- if (!fs.existsSync(gsDest)) {
149
- fs.copyFileSync(gsSource, gsDest);
150
- }
151
-
152
- // 4. Process each HTML file
153
- const modifiedFiles = [];
154
- for (const filePath of htmlFiles) {
155
- const originalHtml = fs.readFileSync(filePath, 'utf8');
156
- // Calculate relative path to assets/js/buzl-tracking.js
157
- let scriptRelPath = 'assets/js/buzl-tracking.js';
158
- if (config && config.pathMode === 'root-absolute') {
159
- scriptRelPath = '/assets/js/buzl-tracking.js';
160
- } else {
161
- const relToTarget = path.relative(path.dirname(filePath), targetJsDir).replace(/\\/g, '/');
162
- scriptRelPath = relToTarget ? `${relToTarget}/buzl-tracking.js` : 'assets/js/buzl-tracking.js';
163
- }
164
-
165
- const modifiedHtml = injectHtml(originalHtml, config, scriptRelPath);
166
- fs.writeFileSync(filePath, modifiedHtml, 'utf8');
167
- modifiedFiles.push({
168
- path: filePath,
169
- relative: path.relative(rootDir, filePath).replace(/\\/g, '/')
170
- });
171
- }
172
-
173
- return {
174
- success: true,
175
- backupDir: backup.backupDir,
176
- modifiedFiles,
177
- runtimeScript: path.relative(rootDir, sdkDestPath),
178
- googleAppsScriptFile: path.relative(rootDir, gsDest)
179
- };
180
- }
181
-
182
- /**
183
- * Cleanly remove all Buzl tracking tags and restore pristine HTML files
184
- */
185
- function removeTracking(rootDir, htmlFiles) {
186
- if (!htmlFiles || !Array.isArray(htmlFiles)) {
187
- const { findHtmlFiles } = require('./scanner');
188
- htmlFiles = findHtmlFiles(rootDir);
189
- }
190
-
191
- // 1. Create a named safety backup before removal
192
- const backup = createBackup(htmlFiles, rootDir, 'Pre-Uninstall Clean Backup');
193
-
194
- const headMarkerRegex = new RegExp(`[\\t ]*${HEAD_START_MARKER}[\\s\\S]*?${HEAD_END_MARKER}\\r?\\n?`, 'g');
195
- const bodyMarkerRegex = new RegExp(`[\\t ]*${BODY_START_MARKER}[\\s\\S]*?${BODY_END_MARKER}\\r?\\n?`, 'g');
196
- const runtimeMarkerRegex = new RegExp(`[\\t ]*${RUNTIME_START_MARKER}[\\s\\S]*?${RUNTIME_END_MARKER}\\r?\\n?`, 'g');
197
-
198
- const cleanedFiles = [];
199
-
200
- for (const filePath of htmlFiles) {
201
- let content = fs.readFileSync(filePath, 'utf8');
202
-
203
- // Strip markers
204
- content = content.replace(headMarkerRegex, '');
205
- content = content.replace(bodyMarkerRegex, '');
206
- content = content.replace(runtimeMarkerRegex, '');
207
-
208
- // Strip data-buzl-track="true" attribute from forms
209
- content = content.replace(/\s*data-buzl-track=["']true["']/gi, '');
210
-
211
- // Clean up any double blank lines before </head> or </body>
212
- content = content.replace(/(\r?\n\s*){2,}<\/head>/i, '\n</head>');
213
- content = content.replace(/(\r?\n\s*){2,}<\/body>/i, '\n</body>');
214
-
215
- fs.writeFileSync(filePath, content, 'utf8');
216
- cleanedFiles.push({
217
- path: filePath,
218
- relative: path.relative(rootDir, filePath)
219
- });
220
- }
221
-
222
- return {
223
- success: true,
224
- backupDir: backup.backupDir,
225
- cleanedFiles,
226
- message: `Successfully removed all tracking from ${cleanedFiles.length} file(s). Clean safety backup saved in ${path.basename(backup.backupDir)}.`
227
- };
228
- }
229
-
230
- /**
231
- * Selectively remove a single tracking service or configuration without breaking other services
232
- */
233
- function removeService(rootDir, serviceName) {
234
- const normService = (serviceName || '').toLowerCase().trim();
235
- const { scanProject } = require('./scanner');
236
- const scan = scanProject(rootDir);
237
- const htmlFiles = scan.files.map(f => f.filePath);
238
-
239
- if (htmlFiles.length === 0) {
240
- return { success: false, message: 'No HTML files found in project.' };
241
- }
242
-
243
- // If full uninstall requested
244
- if (normService === 'all' || normService === 'everything') {
245
- return removeTracking(rootDir, htmlFiles);
246
- }
247
-
248
- // 1. Create safety snapshot backup before selective removal
249
- const backup = createBackup(htmlFiles, rootDir, `Pre-Remove ${normService.toUpperCase()} Backup`);
250
-
251
- // 2. Derive new configuration by blanking the selected service
252
- const current = scan.existingConfig || {};
253
- const newConfig = {
254
- gtmId: normService === 'gtm' ? '' : (current.gtmId || (normService !== 'gtm' && scan.liveState.gtm.id) || ''),
255
- enableDeferred: current.enableDeferred !== false,
256
- metaPixelId: normService === 'meta' ? '' : (current.metaPixelId || (normService !== 'meta' && scan.liveState.meta.id) || ''),
257
- trackMetaLeadEvent: normService === 'meta' ? false : !!current.trackMetaLeadEvent,
258
- googleSheetUrl: normService === 'sheets' ? '' : (current.googleSheetUrl || ''),
259
- siteLocation: current.siteLocation || scan.detectedLocation || '',
260
- dynamicFields: current.dynamicFields || [],
261
- buzlCapi: {
262
- endpoint: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.endpoint) || ''),
263
- authUser: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.authUser) || ''),
264
- authPass: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.authPass) || '')
265
- },
266
- zoho: {
267
- endpoint: normService === 'zoho' ? '' : ((current.zoho && current.zoho.endpoint) || ''),
268
- xnQsjsdp: normService === 'zoho' ? '' : ((current.zoho && current.zoho.xnQsjsdp) || ''),
269
- xmIwtLD: normService === 'zoho' ? '' : ((current.zoho && current.zoho.xmIwtLD) || ''),
270
- fields: normService === 'zoho' ? {} : ((current.zoho && current.zoho.fields) || {})
271
- },
272
- whatsappNumber: normService === 'whatsapp' ? '' : (current.whatsappNumber || (current.whatsapp && current.whatsapp.number) || '')
273
- };
274
-
275
- // 3. Inject updated HTML files
276
- const targetJsDir = path.join(rootDir, 'assets', 'js');
277
- const modifiedFiles = [];
278
-
279
- for (const filePath of htmlFiles) {
280
- const originalHtml = fs.readFileSync(filePath, 'utf8');
281
- let scriptRelPath = 'assets/js/buzl-tracking.js';
282
- if (current && current.pathMode === 'root-absolute') {
283
- scriptRelPath = '/assets/js/buzl-tracking.js';
284
- } else {
285
- const relToTarget = path.relative(path.dirname(filePath), targetJsDir).replace(/\\/g, '/');
286
- scriptRelPath = relToTarget ? `${relToTarget}/buzl-tracking.js` : 'assets/js/buzl-tracking.js';
287
- }
288
-
289
- const modifiedHtml = injectHtml(originalHtml, newConfig, scriptRelPath);
290
- fs.writeFileSync(filePath, modifiedHtml, 'utf8');
291
- modifiedFiles.push({
292
- path: filePath,
293
- relative: path.relative(rootDir, filePath).replace(/\\/g, '/')
294
- });
295
- }
296
-
297
- const serviceLabels = {
298
- gtm: 'Google Tag Manager (GTM)',
299
- meta: 'Meta Pixel & Client CAPI',
300
- capi: 'Buzl CAPI Server-Side Endpoint',
301
- sheets: 'Google Sheets Direct Sync',
302
- zoho: 'Zoho CRM Web-to-Lead',
303
- whatsapp: 'WhatsApp Handoff'
304
- };
305
-
306
- return {
307
- success: true,
308
- service: normService,
309
- serviceLabel: serviceLabels[normService] || normService,
310
- backupDir: backup.backupDir,
311
- modifiedFiles,
312
- message: `Successfully removed ${serviceLabels[normService] || normService} from site. Clean safety backup saved in ${path.basename(backup.backupDir)}.`
313
- };
314
- }
315
-
316
- module.exports = {
317
- injectHtml,
318
- applyInjection,
319
- removeTracking,
320
- removeService
321
- };
1
+ /**
2
+ * Safe HTML & Tracking Injector
3
+ * Injects GTM, Meta Pixel, Form Sync runtime, and configuration
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const {
8
+ HEAD_START_MARKER,
9
+ HEAD_END_MARKER,
10
+ BODY_START_MARKER,
11
+ BODY_END_MARKER,
12
+ generateHeadSnippet,
13
+ generateBodySnippet
14
+ } = require('../templates/gtm-meta-snippets');
15
+ const { createBackup } = require('./rollback');
16
+
17
+ const RUNTIME_START_MARKER = '<!-- BUZL_TRACKING_RUNTIME_START -->';
18
+ const RUNTIME_END_MARKER = '<!-- BUZL_TRACKING_RUNTIME_END -->';
19
+
20
+ /**
21
+ * Generate runtime script tag & inline configuration
22
+ */
23
+ function generateRuntimeSnippet(config, scriptRelPath) {
24
+ const zohoObj = config.zoho || {};
25
+ const zohoXn = (config.zohoXnqsjsdp || zohoObj.xnQsjsdp || '').trim();
26
+ const zohoEndpoint = zohoXn ? (config.zohoEndpoint || zohoObj.endpoint || 'https://crm.zoho.in/crm/WebToLeadForm') : '';
27
+ const zohoXm = zohoXn ? (config.zohoXmiwtld || zohoObj.xmIwtLD || '') : '';
28
+ const zohoFields = zohoXn ? (config.zohoFields || zohoObj.fields || { lastName: 'Last Name', phone: 'Mobile', location: 'City' }) : {};
29
+
30
+ const clientConfig = {
31
+ enableGTM: !!(config.gtmId && config.gtmId.trim()),
32
+ gtmEvent: config.gtmEvent || 'lead_form_submitted',
33
+ enableMeta: !!(config.metaPixelId && config.metaPixelId.trim()),
34
+ trackMetaLeadEvent: !!config.trackMetaLeadEvent,
35
+ googleSheetUrl: config.googleSheetUrl || '',
36
+ siteLocation: config.siteLocation || '',
37
+ zoho: {
38
+ endpoint: zohoEndpoint,
39
+ xnQsjsdp: zohoXn,
40
+ xmIwtLD: zohoXm,
41
+ fields: zohoFields
42
+ },
43
+ buzlCapi: {
44
+ endpoint: (config.buzlCapi && config.buzlCapi.endpoint) || config.buzlCapiEndpoint || '',
45
+ authUser: (config.buzlCapi && config.buzlCapi.authUser) || config.buzlCapiUser || '',
46
+ authPass: (config.buzlCapi && config.buzlCapi.authPass) || config.buzlCapiPass || ''
47
+ },
48
+ whatsapp: {
49
+ number: config.whatsappNumber || (config.whatsapp && config.whatsapp.number) || '',
50
+ template: config.whatsappTemplate || (config.whatsapp && config.whatsapp.template) || 'Hi, I submitted an inquiry from {name} in {location}.'
51
+ },
52
+ safetyTimeoutMs: config.safetyTimeoutMs || 800
53
+ };
54
+
55
+ return [
56
+ RUNTIME_START_MARKER,
57
+ ' <script id="buzl-tracking-config">',
58
+ ` window.__BUZL_CONFIG__ = ${JSON.stringify(clientConfig, null, 2)};`,
59
+ ' </script>',
60
+ ` <script src="${scriptRelPath}" defer></script>`,
61
+ RUNTIME_END_MARKER
62
+ ].join('\n');
63
+ }
64
+
65
+ /**
66
+ * Injects tracking into a single HTML string
67
+ */
68
+ function injectHtml(htmlContent, config, scriptRelPath = 'assets/js/buzl-tracking.js') {
69
+ let content = htmlContent;
70
+
71
+ const hasHeadTrackers = !!((config.gtmId && config.gtmId.trim()) || (config.metaPixelId && config.metaPixelId.trim()));
72
+ const headSnippet = hasHeadTrackers ? generateHeadSnippet(config) : '';
73
+ const bodySnippet = hasHeadTrackers ? generateBodySnippet(config) : '';
74
+ const runtimeSnippet = generateRuntimeSnippet(config, scriptRelPath);
75
+
76
+ // 1. Head snippet replacement or injection
77
+ const headMarkerRegex = new RegExp(`[\\t ]*${HEAD_START_MARKER}[\\s\\S]*?${HEAD_END_MARKER}\\r?\\n?`, 'g');
78
+ if (headMarkerRegex.test(content)) {
79
+ content = content.replace(headMarkerRegex, hasHeadTrackers ? `${headSnippet}\n` : '');
80
+ } else if (hasHeadTrackers) {
81
+ if (/<\/head>/i.test(content)) {
82
+ content = content.replace(/<\/head>/i, `${headSnippet}\n</head>`);
83
+ } else if (/<head[\s>]/i.test(content)) {
84
+ content = content.replace(/(<head[\s>][^>]*>)/i, `$1\n${headSnippet}`);
85
+ }
86
+ }
87
+
88
+ // 2. Body noscript snippet replacement or injection
89
+ const bodyMarkerRegex = new RegExp(`[\\t ]*${BODY_START_MARKER}[\\s\\S]*?${BODY_END_MARKER}\\r?\\n?`, 'g');
90
+ if (bodyMarkerRegex.test(content)) {
91
+ content = content.replace(bodyMarkerRegex, hasHeadTrackers ? `${bodySnippet}\n` : '');
92
+ } else if (hasHeadTrackers) {
93
+ if (/<body[\s>]/i.test(content)) {
94
+ content = content.replace(/(<body[\s>][^>]*>)/i, `$1\n${bodySnippet}`);
95
+ }
96
+ }
97
+
98
+ // 3. Runtime script replacement or injection
99
+ const runtimeMarkerRegex = new RegExp(`${RUNTIME_START_MARKER}[\\s\\S]*?${RUNTIME_END_MARKER}`, 'g');
100
+ if (runtimeMarkerRegex.test(content)) {
101
+ content = content.replace(runtimeMarkerRegex, runtimeSnippet);
102
+ } else if (/<\/body>/i.test(content)) {
103
+ content = content.replace(/<\/body>/i, `${runtimeSnippet}\n</body>`);
104
+ } else {
105
+ content += `\n${runtimeSnippet}`;
106
+ }
107
+
108
+ // 4. Form tag attribute injection (adds data-buzl-track="true" to forms if missing)
109
+ content = content.replace(/<form\b(?![^>]*\bdata-buzl-)([^>]*)>/gi, (match, p1) => {
110
+ return `<form data-buzl-track="true"${p1}>`;
111
+ });
112
+
113
+ return content;
114
+ }
115
+
116
+ /**
117
+ * Apply injection across the whole project
118
+ */
119
+ function applyInjection(rootDir, htmlFiles, config) {
120
+ // If whatsapp number is not explicitly configured, fallback to auto-detected number from buttons
121
+ if (!config.whatsappNumber && !(config.whatsapp && config.whatsapp.number)) {
122
+ try {
123
+ const { scanProject } = require('./scanner');
124
+ const scan = scanProject(rootDir);
125
+ if (scan && scan.detectedWhatsapp) {
126
+ config = Object.assign({}, config, {
127
+ whatsappNumber: scan.detectedWhatsapp,
128
+ whatsapp: Object.assign({}, config.whatsapp || {}, { number: scan.detectedWhatsapp, autoDetected: true })
129
+ });
130
+ }
131
+ } catch (e) {}
132
+ }
133
+
134
+ // 1. Ensure backup first
135
+ const backup = createBackup(htmlFiles, rootDir);
136
+
137
+ // 2. Deploy runtime SDK script into assets/js/
138
+ const targetJsDir = path.join(rootDir, 'assets', 'js');
139
+ fs.mkdirSync(targetJsDir, { recursive: true });
140
+
141
+ const sdkSourcePath = path.join(__dirname, '..', 'templates', 'buzl-tracking.js');
142
+ const sdkDestPath = path.join(targetJsDir, 'buzl-tracking.js');
143
+ fs.copyFileSync(sdkSourcePath, sdkDestPath);
144
+
145
+ // 3. Deploy GoogleAppsScript.gs to root or reference folder
146
+ const gsSource = path.join(__dirname, '..', 'templates', 'GoogleAppsScript.gs');
147
+ const gsDest = path.join(rootDir, 'Buzl_GoogleAppsScript_Template.gs');
148
+ if (!fs.existsSync(gsDest)) {
149
+ fs.copyFileSync(gsSource, gsDest);
150
+ }
151
+
152
+ // 4. Process each HTML file
153
+ const modifiedFiles = [];
154
+ for (const filePath of htmlFiles) {
155
+ const originalHtml = fs.readFileSync(filePath, 'utf8');
156
+ // Calculate relative path to assets/js/buzl-tracking.js
157
+ let scriptRelPath = 'assets/js/buzl-tracking.js';
158
+ if (config && config.pathMode === 'root-absolute') {
159
+ scriptRelPath = '/assets/js/buzl-tracking.js';
160
+ } else {
161
+ const relToTarget = path.relative(path.dirname(filePath), targetJsDir).replace(/\\/g, '/');
162
+ scriptRelPath = relToTarget ? `${relToTarget}/buzl-tracking.js` : 'assets/js/buzl-tracking.js';
163
+ }
164
+
165
+ const modifiedHtml = injectHtml(originalHtml, config, scriptRelPath);
166
+ fs.writeFileSync(filePath, modifiedHtml, 'utf8');
167
+ modifiedFiles.push({
168
+ path: filePath,
169
+ relative: path.relative(rootDir, filePath).replace(/\\/g, '/')
170
+ });
171
+ }
172
+
173
+ return {
174
+ success: true,
175
+ backupDir: backup.backupDir,
176
+ modifiedFiles,
177
+ runtimeScript: path.relative(rootDir, sdkDestPath),
178
+ googleAppsScriptFile: path.relative(rootDir, gsDest)
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Cleanly remove all Buzl tracking tags and restore pristine HTML files
184
+ */
185
+ function removeTracking(rootDir, htmlFiles) {
186
+ if (!htmlFiles || !Array.isArray(htmlFiles)) {
187
+ const { findHtmlFiles } = require('./scanner');
188
+ htmlFiles = findHtmlFiles(rootDir);
189
+ }
190
+
191
+ // 1. Create a named safety backup before removal
192
+ const backup = createBackup(htmlFiles, rootDir, 'Pre-Uninstall Clean Backup');
193
+
194
+ const headMarkerRegex = new RegExp(`[\\t ]*${HEAD_START_MARKER}[\\s\\S]*?${HEAD_END_MARKER}\\r?\\n?`, 'g');
195
+ const bodyMarkerRegex = new RegExp(`[\\t ]*${BODY_START_MARKER}[\\s\\S]*?${BODY_END_MARKER}\\r?\\n?`, 'g');
196
+ const runtimeMarkerRegex = new RegExp(`[\\t ]*${RUNTIME_START_MARKER}[\\s\\S]*?${RUNTIME_END_MARKER}\\r?\\n?`, 'g');
197
+
198
+ const cleanedFiles = [];
199
+
200
+ for (const filePath of htmlFiles) {
201
+ let content = fs.readFileSync(filePath, 'utf8');
202
+
203
+ // Strip markers
204
+ content = content.replace(headMarkerRegex, '');
205
+ content = content.replace(bodyMarkerRegex, '');
206
+ content = content.replace(runtimeMarkerRegex, '');
207
+
208
+ // Strip data-buzl-track="true" attribute from forms
209
+ content = content.replace(/\s*data-buzl-track=["']true["']/gi, '');
210
+
211
+ // Clean up any double blank lines before </head> or </body>
212
+ content = content.replace(/(\r?\n\s*){2,}<\/head>/i, '\n</head>');
213
+ content = content.replace(/(\r?\n\s*){2,}<\/body>/i, '\n</body>');
214
+
215
+ fs.writeFileSync(filePath, content, 'utf8');
216
+ cleanedFiles.push({
217
+ path: filePath,
218
+ relative: path.relative(rootDir, filePath)
219
+ });
220
+ }
221
+
222
+ return {
223
+ success: true,
224
+ backupDir: backup.backupDir,
225
+ cleanedFiles,
226
+ message: `Successfully removed all tracking from ${cleanedFiles.length} file(s). Clean safety backup saved in ${path.basename(backup.backupDir)}.`
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Selectively remove a single tracking service or configuration without breaking other services
232
+ */
233
+ function removeService(rootDir, serviceName) {
234
+ const normService = (serviceName || '').toLowerCase().trim();
235
+ const { scanProject } = require('./scanner');
236
+ const scan = scanProject(rootDir);
237
+ const htmlFiles = scan.files.map(f => f.filePath);
238
+
239
+ if (htmlFiles.length === 0) {
240
+ return { success: false, message: 'No HTML files found in project.' };
241
+ }
242
+
243
+ // If full uninstall requested
244
+ if (normService === 'all' || normService === 'everything') {
245
+ return removeTracking(rootDir, htmlFiles);
246
+ }
247
+
248
+ // 1. Create safety snapshot backup before selective removal
249
+ const backup = createBackup(htmlFiles, rootDir, `Pre-Remove ${normService.toUpperCase()} Backup`);
250
+
251
+ // 2. Derive new configuration by blanking the selected service
252
+ const current = scan.existingConfig || {};
253
+ const newConfig = {
254
+ gtmId: normService === 'gtm' ? '' : (current.gtmId || (normService !== 'gtm' && scan.liveState.gtm.id) || ''),
255
+ enableDeferred: current.enableDeferred !== false,
256
+ metaPixelId: normService === 'meta' ? '' : (current.metaPixelId || (normService !== 'meta' && scan.liveState.meta.id) || ''),
257
+ trackMetaLeadEvent: normService === 'meta' ? false : !!current.trackMetaLeadEvent,
258
+ googleSheetUrl: normService === 'sheets' ? '' : (current.googleSheetUrl || ''),
259
+ siteLocation: current.siteLocation || scan.detectedLocation || '',
260
+ dynamicFields: current.dynamicFields || [],
261
+ buzlCapi: {
262
+ endpoint: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.endpoint) || ''),
263
+ authUser: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.authUser) || ''),
264
+ authPass: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.authPass) || '')
265
+ },
266
+ zoho: {
267
+ endpoint: normService === 'zoho' ? '' : ((current.zoho && current.zoho.endpoint) || ''),
268
+ xnQsjsdp: normService === 'zoho' ? '' : ((current.zoho && current.zoho.xnQsjsdp) || ''),
269
+ xmIwtLD: normService === 'zoho' ? '' : ((current.zoho && current.zoho.xmIwtLD) || ''),
270
+ fields: normService === 'zoho' ? {} : ((current.zoho && current.zoho.fields) || {})
271
+ },
272
+ whatsappNumber: normService === 'whatsapp' ? '' : (current.whatsappNumber || (current.whatsapp && current.whatsapp.number) || '')
273
+ };
274
+
275
+ // 3. Inject updated HTML files
276
+ const targetJsDir = path.join(rootDir, 'assets', 'js');
277
+ const modifiedFiles = [];
278
+
279
+ for (const filePath of htmlFiles) {
280
+ const originalHtml = fs.readFileSync(filePath, 'utf8');
281
+ let scriptRelPath = 'assets/js/buzl-tracking.js';
282
+ if (current && current.pathMode === 'root-absolute') {
283
+ scriptRelPath = '/assets/js/buzl-tracking.js';
284
+ } else {
285
+ const relToTarget = path.relative(path.dirname(filePath), targetJsDir).replace(/\\/g, '/');
286
+ scriptRelPath = relToTarget ? `${relToTarget}/buzl-tracking.js` : 'assets/js/buzl-tracking.js';
287
+ }
288
+
289
+ const modifiedHtml = injectHtml(originalHtml, newConfig, scriptRelPath);
290
+ fs.writeFileSync(filePath, modifiedHtml, 'utf8');
291
+ modifiedFiles.push({
292
+ path: filePath,
293
+ relative: path.relative(rootDir, filePath).replace(/\\/g, '/')
294
+ });
295
+ }
296
+
297
+ const serviceLabels = {
298
+ gtm: 'Google Tag Manager (GTM)',
299
+ meta: 'Meta Pixel & Client CAPI',
300
+ capi: 'Buzl CAPI Server-Side Endpoint',
301
+ sheets: 'Google Sheets Direct Sync',
302
+ zoho: 'Zoho CRM Web-to-Lead',
303
+ whatsapp: 'WhatsApp Handoff'
304
+ };
305
+
306
+ return {
307
+ success: true,
308
+ service: normService,
309
+ serviceLabel: serviceLabels[normService] || normService,
310
+ backupDir: backup.backupDir,
311
+ modifiedFiles,
312
+ message: `Successfully removed ${serviceLabels[normService] || normService} from site. Clean safety backup saved in ${path.basename(backup.backupDir)}.`
313
+ };
314
+ }
315
+
316
+ module.exports = {
317
+ injectHtml,
318
+ applyInjection,
319
+ removeTracking,
320
+ removeService
321
+ };