@contentsquare/wizard 2.0.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/dist/cli.js ADDED
@@ -0,0 +1,1187 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync } from "fs";
5
+ import { fileURLToPath as fileURLToPath2 } from "url";
6
+ import { Command } from "commander";
7
+
8
+ // src/commands/status.ts
9
+ import * as clack from "@clack/prompts";
10
+ import chalk2 from "chalk";
11
+ import path from "path";
12
+ import fs from "fs";
13
+
14
+ // src/commands/sorcerer.ts
15
+ import chalk from "chalk";
16
+ var SORCERER = chalk.magenta(`
17
+ ____
18
+ .'* *.'
19
+ __/_*_*(_
20
+ / _______ \\
21
+ _\\_)/___\\(_/_
22
+ / _((\\- -/))_ \\
23
+ \\ \\())(-)(()/ /
24
+ ' \\(((()))/ '
25
+ / ' \\)).))/ ' \\
26
+ / _ \\ - | - /_ \\
27
+ ( ( .;''';. .' )
28
+ _\\"__ / )\\ __"/_
29
+ \\/ \\ ' / \\/
30
+ .' '...' ' )
31
+ / / | \\ \\
32
+ / . . . \\
33
+ / . . \\
34
+ / / | \\ \\
35
+ .' / b '. '.
36
+ _.-' / Bb '-. '-._
37
+ _.-' | BBb '-. '-.
38
+ (________mrf\\____.dBBBb.________)____)
39
+ `) + chalk.bold.magenta("\n CS Sorcerer") + chalk.dim('\n "Your tag shall be cast."\n');
40
+
41
+ // src/commands/status.ts
42
+ async function status(options) {
43
+ process.stdout.write(SORCERER + "\n");
44
+ const targetDir = path.resolve(options.dir);
45
+ const stateFile = path.join(targetDir, ".cs-wizard", "state.json");
46
+ if (!fs.existsSync(stateFile)) {
47
+ clack.log.error(
48
+ `No wizard state found. Run ${chalk2.cyan("npx @contentsquare/wizard start")} first.`
49
+ );
50
+ process.exit(1);
51
+ }
52
+ const state = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
53
+ clack.intro(chalk2.bold("Contentsquare Wizard Status"));
54
+ clack.log.info(`Tag ID: ${chalk2.cyan(state.tagId ?? "unknown")}`);
55
+ if (state.projectId) {
56
+ clack.log.info(`Project ID: ${chalk2.cyan(state.projectId)}`);
57
+ }
58
+ clack.log.info("");
59
+ const report = readLastReport(targetDir);
60
+ if (!report) {
61
+ clack.log.warn("No verification run yet.");
62
+ clack.log.info(
63
+ `Run ${chalk2.cyan("npx @contentsquare/wizard verify")} to check the tag in a real browser.`
64
+ );
65
+ clack.outro("");
66
+ return;
67
+ }
68
+ const ok = chalk2.green("\u2713");
69
+ const no = chalk2.red("\u2717");
70
+ const warn = chalk2.yellow("!");
71
+ clack.log.step(chalk2.bold("Latest verification"));
72
+ clack.log.info(`URL: ${chalk2.cyan(report.url)}`);
73
+ clack.log.info(`${report.tagScriptLoaded ? ok : no} Tag script loaded`);
74
+ clack.log.info(
75
+ `${report.initialPageview ? ok : no} First pageview sent on load`
76
+ );
77
+ if (report.navigations.length === 0) {
78
+ clack.log.info(`${warn} No in-app route changes recorded`);
79
+ } else {
80
+ const icon = report.navigationsWithPageview === report.navigations.length ? ok : warn;
81
+ clack.log.info(
82
+ `${icon} Pageviews on navigation: ${report.navigationsWithPageview}/${report.navigations.length}`
83
+ );
84
+ }
85
+ clack.log.info(
86
+ report.cspViolations.length === 0 ? `${ok} No CSP violations` : `${no} CSP violations: ${report.cspViolations.length}`
87
+ );
88
+ clack.log.info("");
89
+ if (report.result === "pass") {
90
+ clack.log.success(chalk2.green("Result: pass"));
91
+ } else if (report.result === "pass-with-recommendation") {
92
+ clack.log.warn(chalk2.yellow("Result: pass with a recommendation"));
93
+ if (report.recommendation) clack.log.info(` ${report.recommendation}`);
94
+ } else {
95
+ clack.log.error(chalk2.red("Result: fail"));
96
+ }
97
+ clack.outro("");
98
+ }
99
+ function readLastReport(targetDir) {
100
+ const reportFile = path.join(targetDir, ".cs-wizard", "last-report.json");
101
+ if (!fs.existsSync(reportFile)) return null;
102
+ try {
103
+ return JSON.parse(fs.readFileSync(reportFile, "utf-8"));
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ // src/commands/verify/index.ts
110
+ import * as clack4 from "@clack/prompts";
111
+ import chalk7 from "chalk";
112
+ import path5 from "path";
113
+ import fs5 from "fs";
114
+
115
+ // src/tools/state.ts
116
+ import fs2 from "fs";
117
+ import path2 from "path";
118
+ var STATE_FILE = ".cs-wizard/state.json";
119
+ function readState(dir) {
120
+ const stateFile = path2.join(dir, STATE_FILE);
121
+ if (fs2.existsSync(stateFile)) {
122
+ try {
123
+ return JSON.parse(fs2.readFileSync(stateFile, "utf-8"));
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+
131
+ // src/tools/tag-id.ts
132
+ function isHashedTagId(value) {
133
+ return /^[0-9a-f]{10,20}$/.test(value.trim());
134
+ }
135
+ function validateTagId(value) {
136
+ return isHashedTagId(value) ? void 0 : "Tag ID is a lowercase hex string (~13 chars, e.g. 81c677ba742d7).";
137
+ }
138
+ function normalizeTagId(tagId) {
139
+ return tagId.trim().toLowerCase();
140
+ }
141
+
142
+ // src/commands/verify/logger.ts
143
+ import * as clack2 from "@clack/prompts";
144
+ function makeLogger(humanOutputToStderr) {
145
+ const stream = (chunk) => process.stderr.write(chunk);
146
+ if (humanOutputToStderr) {
147
+ const toErr = (m) => process.stderr.write(m + "\n");
148
+ return {
149
+ info: toErr,
150
+ step: toErr,
151
+ error: toErr,
152
+ success: toErr,
153
+ warn: toErr,
154
+ stream
155
+ };
156
+ }
157
+ return {
158
+ info: (m) => clack2.log.info(m),
159
+ step: (m) => clack2.log.step(m),
160
+ error: (m) => clack2.log.error(m),
161
+ success: (m) => clack2.log.success(m),
162
+ warn: (m) => clack2.log.warn(m),
163
+ stream
164
+ };
165
+ }
166
+ function fail(message) {
167
+ process.stderr.write(message + "\n");
168
+ process.exit(1);
169
+ }
170
+ function emitStdout(text3) {
171
+ process.stdout.write(text3);
172
+ }
173
+
174
+ // src/commands/verify/browser.ts
175
+ import * as clack3 from "@clack/prompts";
176
+ import chalk3 from "chalk";
177
+ import { spawn } from "child_process";
178
+ import { createRequire } from "module";
179
+ import { chromium } from "playwright-core";
180
+
181
+ // src/commands/verify/artifact.ts
182
+ import path3 from "path";
183
+ import fs3 from "fs";
184
+ function writeArtifact(targetDir, fileName, data) {
185
+ try {
186
+ const dir = path3.join(targetDir, ".cs-wizard");
187
+ fs3.mkdirSync(dir, { recursive: true });
188
+ fs3.writeFileSync(path3.join(dir, fileName), JSON.stringify(data, null, 2));
189
+ } catch {
190
+ }
191
+ }
192
+
193
+ // src/commands/verify/browser.ts
194
+ var PROXY_CERT_RE = /self.signed certificate|unable to (?:verify|get local issuer)|CERT_|SSL|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|getaddrinfo|tunneling socket|proxy/i;
195
+ var BROWSER_FAILURE_EXIT_CODE = {
196
+ BROWSER_LAUNCH_FAILED: 1,
197
+ NO_BROWSER: 2,
198
+ MISSING_LIBS: 3,
199
+ BLOCKED_LAUNCH: 4,
200
+ NO_DISPLAY: 5
201
+ };
202
+ async function closeAndExit(browser, code) {
203
+ await browser.close().catch(() => {
204
+ });
205
+ process.exit(code);
206
+ }
207
+ async function acquireBrowser(options, log4, targetDir) {
208
+ let result = await tryLaunch();
209
+ if (result.browser) return result.browser;
210
+ let remedy = decideRemedy(result.attempts);
211
+ if (remedy === "missing-libs") {
212
+ failBrowser(
213
+ options,
214
+ log4,
215
+ targetDir,
216
+ "MISSING_LIBS",
217
+ result.attempts,
218
+ () => printMissingLibsHelp(log4, result.attempts)
219
+ );
220
+ }
221
+ if (remedy === "blocked") {
222
+ failBrowser(
223
+ options,
224
+ log4,
225
+ targetDir,
226
+ "BLOCKED_LAUNCH",
227
+ result.attempts,
228
+ () => printBlockedHelp(log4, result.attempts)
229
+ );
230
+ }
231
+ if (remedy === "missing") {
232
+ const install = await ensureBrowser(options, log4);
233
+ if (!install.ok) {
234
+ const attempts = result.attempts;
235
+ if (install.proxyHint) {
236
+ failBrowser(
237
+ options,
238
+ log4,
239
+ targetDir,
240
+ "NO_BROWSER",
241
+ attempts,
242
+ () => printProxyHelp(log4)
243
+ );
244
+ }
245
+ failBrowser(
246
+ options,
247
+ log4,
248
+ targetDir,
249
+ "NO_BROWSER",
250
+ attempts,
251
+ () => printBrowserHelp(log4, lastError(attempts))
252
+ );
253
+ }
254
+ result = await tryLaunch();
255
+ if (result.browser) return result.browser;
256
+ remedy = decideRemedy(result.attempts);
257
+ if (remedy === "missing-libs") {
258
+ failBrowser(
259
+ options,
260
+ log4,
261
+ targetDir,
262
+ "MISSING_LIBS",
263
+ result.attempts,
264
+ () => printMissingLibsHelp(log4, result.attempts)
265
+ );
266
+ }
267
+ failBrowser(
268
+ options,
269
+ log4,
270
+ targetDir,
271
+ "BROWSER_LAUNCH_FAILED",
272
+ result.attempts,
273
+ () => printBrowserHelp(log4, lastError(result.attempts))
274
+ );
275
+ }
276
+ failBrowser(
277
+ options,
278
+ log4,
279
+ targetDir,
280
+ "BROWSER_LAUNCH_FAILED",
281
+ result.attempts,
282
+ () => {
283
+ log4.error(`Could not launch a browser: ${lastError(result.attempts)}`);
284
+ }
285
+ );
286
+ }
287
+ async function tryLaunch() {
288
+ const configs = [
289
+ { label: "chrome", channel: "chrome" },
290
+ { label: "msedge", channel: "msedge" },
291
+ { label: "chromium" }
292
+ // bundled, if `playwright-core install chromium` was run
293
+ ];
294
+ const attempts = [];
295
+ for (const cfg of configs) {
296
+ try {
297
+ const browser = await chromium.launch({
298
+ headless: false,
299
+ ...cfg.channel ? { channel: cfg.channel } : {}
300
+ });
301
+ return { browser, attempts };
302
+ } catch (err) {
303
+ const message = err?.message ?? String(err);
304
+ attempts.push({
305
+ label: cfg.label,
306
+ channel: cfg.channel,
307
+ klass: classifyLaunchError(message),
308
+ message
309
+ });
310
+ }
311
+ }
312
+ return { browser: null, attempts };
313
+ }
314
+ function classifyLaunchError(message) {
315
+ if (/loading shared librar|libnss3|libnspr4|libatk|libgbm|libasound|libgtk|libx11|GLIBC/i.test(
316
+ message
317
+ ))
318
+ return "missing-libs";
319
+ if (/Executable doesn't exist|Chromium distribution|was not found|ENOENT|playwright install/i.test(
320
+ message
321
+ ))
322
+ return "missing";
323
+ if (/policy|enterprise|SingletonLock|ProcessSingleton|cannot create default profile|user data directory is already in use|DevToolsActivePort|Target page, context or browser has been closed/i.test(
324
+ message
325
+ ))
326
+ return "blocked";
327
+ return "unknown";
328
+ }
329
+ function decideRemedy(attempts) {
330
+ const chromiumAttempt = attempts.find((a) => a.label === "chromium");
331
+ if (chromiumAttempt?.klass === "missing") return "missing";
332
+ if (attempts.some((a) => a.klass === "missing-libs")) return "missing-libs";
333
+ if (attempts.every((a) => a.klass === "missing")) return "missing";
334
+ if (attempts.some((a) => a.klass === "blocked")) return "blocked";
335
+ return "unknown";
336
+ }
337
+ function lastError(attempts) {
338
+ return attempts[attempts.length - 1]?.message ?? "unknown error";
339
+ }
340
+ function isBrowserGoneError(message) {
341
+ return /Target (?:page, context or browser|closed)|browser has been closed|Browser closed|crash|disconnected/i.test(
342
+ message
343
+ );
344
+ }
345
+ function failBrowser(options, log4, targetDir, code, attempts, printHelp) {
346
+ writeBrowserFailure(targetDir, code, attempts);
347
+ printHelp();
348
+ if (options.json) {
349
+ emitStdout(JSON.stringify({ error: code }) + "\n");
350
+ }
351
+ process.exit(BROWSER_FAILURE_EXIT_CODE[code]);
352
+ }
353
+ function writeBrowserFailure(targetDir, code, attempts) {
354
+ writeArtifact(targetDir, "last-browser-error.json", {
355
+ error: code,
356
+ platform: `${process.platform} ${process.arch}`,
357
+ display: process.env.DISPLAY ?? null,
358
+ attempts: attempts.map((a) => ({
359
+ label: a.label,
360
+ class: a.klass,
361
+ message: a.message
362
+ })),
363
+ ts: (/* @__PURE__ */ new Date()).toISOString()
364
+ });
365
+ }
366
+ function handleNoDisplay(options, log4, targetDir) {
367
+ writeBrowserFailure(targetDir, "NO_DISPLAY", []);
368
+ log4.error(
369
+ "No display detected. `verify` opens a real browser window and needs a desktop session."
370
+ );
371
+ log4.info(
372
+ "Run this on a machine with a graphical desktop \u2014 not a headless server or a plain SSH session."
373
+ );
374
+ if (options.json) emitStdout(JSON.stringify({ error: "NO_DISPLAY" }) + "\n");
375
+ process.exit(BROWSER_FAILURE_EXIT_CODE.NO_DISPLAY);
376
+ }
377
+ async function ensureBrowser(options, log4) {
378
+ if (!options.installBrowser) {
379
+ if (options.json) {
380
+ log4.error(
381
+ "NO_BROWSER: no Chrome/Edge/Chromium found. Get the user's consent, then re-run this exact command with --install-browser added."
382
+ );
383
+ return { ok: false, proxyHint: false };
384
+ }
385
+ const consent = await clack3.confirm({
386
+ message: "No browser found. Download the Chromium engine now? (~280 MB, one-time)",
387
+ initialValue: true
388
+ });
389
+ if (clack3.isCancel(consent) || !consent)
390
+ return { ok: false, proxyHint: false };
391
+ }
392
+ return installChromium(log4);
393
+ }
394
+ function installChromium(log4) {
395
+ const require2 = createRequire(import.meta.url);
396
+ let cliPath;
397
+ try {
398
+ cliPath = require2.resolve("playwright-core/cli.js");
399
+ } catch {
400
+ log4.error("Could not locate playwright-core CLI to install Chromium.");
401
+ return Promise.resolve({ ok: false, proxyHint: false });
402
+ }
403
+ log4.step("Downloading Chromium (one-time)\u2026");
404
+ return new Promise((resolve) => {
405
+ const child = spawn(process.execPath, [cliPath, "install", "chromium"], {
406
+ // Forward proxy / CA / mirror settings so corporate networks can download.
407
+ env: { ...process.env },
408
+ // Never inherit stdout: it is reserved for the JSON report in --json mode.
409
+ stdio: ["ignore", "pipe", "pipe"]
410
+ });
411
+ let output = "";
412
+ const onData = (chunk) => {
413
+ const text3 = chunk.toString();
414
+ output += text3;
415
+ log4.stream(text3);
416
+ };
417
+ child.stdout?.on("data", onData);
418
+ child.stderr?.on("data", onData);
419
+ child.on("error", (err) => {
420
+ output += String(err?.message ?? err);
421
+ resolve({ ok: false, proxyHint: PROXY_CERT_RE.test(output) });
422
+ });
423
+ child.on("close", (code) => {
424
+ resolve({
425
+ ok: code === 0,
426
+ proxyHint: code !== 0 && PROXY_CERT_RE.test(output)
427
+ });
428
+ });
429
+ });
430
+ }
431
+ function printBrowserHelp(log4, errMessage) {
432
+ log4.error("Could not launch a browser.");
433
+ log4.info("Install one of the following, then re-run:");
434
+ log4.info(
435
+ ` \u2022 Google Chrome (recommended) \u2014 ${chalk3.cyan("https://www.google.com/chrome/")}`
436
+ );
437
+ log4.info(
438
+ ` \u2022 or the bundled engine \u2014 ${chalk3.cyan("npx playwright-core install chromium")}`
439
+ );
440
+ log4.info(chalk3.dim(`(${errMessage})`));
441
+ }
442
+ function printMissingLibsHelp(log4, attempts) {
443
+ log4.error("A browser was found but is missing system libraries.");
444
+ log4.info("Install the libraries Chromium needs, then re-run:");
445
+ log4.info(` ${chalk3.cyan("sudo npx playwright-core install-deps chromium")}`);
446
+ log4.info(chalk3.dim(`(${lastError(attempts)})`));
447
+ }
448
+ function printBlockedHelp(log4, attempts) {
449
+ log4.error("A browser was found but refused to launch.");
450
+ log4.info(
451
+ "This is usually an enterprise policy or a profile that is already in use."
452
+ );
453
+ log4.info("Try one of the following, then re-run:");
454
+ log4.info(" \u2022 Close all open Chrome/Edge windows and try again.");
455
+ log4.info(
456
+ ` \u2022 Or use a clean bundled engine \u2014 ${chalk3.cyan("npx playwright-core install chromium")}, then re-run with ${chalk3.cyan("--install-browser")}.`
457
+ );
458
+ log4.info(chalk3.dim(`(${lastError(attempts)})`));
459
+ }
460
+ function printProxyHelp(log4) {
461
+ log4.error(
462
+ "Could not download Chromium \u2014 this looks like a proxy or certificate issue."
463
+ );
464
+ log4.info("If you are behind a corporate proxy, set these and re-run:");
465
+ for (const line of proxyEnvExamples()) {
466
+ log4.info(
467
+ ` ${chalk3.cyan(line.command)}${line.note ? ` ${chalk3.dim(line.note)}` : ""}`
468
+ );
469
+ }
470
+ }
471
+ function proxyEnvExamples() {
472
+ const vars = [
473
+ { name: "HTTPS_PROXY", value: "http://your-proxy:port" },
474
+ { name: "NODE_EXTRA_CA_CERTS", value: "/path/to/corporate-ca.pem" },
475
+ {
476
+ name: "PLAYWRIGHT_DOWNLOAD_HOST",
477
+ value: "http://your-mirror",
478
+ note: "(if your org mirrors the browser CDN)"
479
+ }
480
+ ];
481
+ if (process.platform === "win32") {
482
+ return vars.map((v) => ({
483
+ command: `$env:${v.name}="${v.value}"`,
484
+ note: v.note
485
+ }));
486
+ }
487
+ return vars.map((v) => ({
488
+ command: `export ${v.name}=${v.value}`,
489
+ note: v.note
490
+ }));
491
+ }
492
+
493
+ // src/commands/verify/preflight.ts
494
+ import chalk4 from "chalk";
495
+ import path4 from "path";
496
+ import fs4 from "fs";
497
+ import os from "os";
498
+ import { createRequire as createRequire2 } from "module";
499
+ import { chromium as chromium2 } from "playwright-core";
500
+ var PREFLIGHT_NO_BROWSER_EXIT_CODE = 2;
501
+ async function runPreflight(targetDir, options, log4) {
502
+ const browsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH ?? defaultBrowsersPath();
503
+ let bundledChromiumPath = null;
504
+ let bundledChromiumPresent = false;
505
+ try {
506
+ bundledChromiumPath = chromium2.executablePath();
507
+ bundledChromiumPresent = !!bundledChromiumPath && fs4.existsSync(bundledChromiumPath);
508
+ } catch {
509
+ bundledChromiumPath = null;
510
+ }
511
+ const systemChrome = await probeChannel("chrome");
512
+ const systemEdge = await probeChannel("msedge");
513
+ const bundledChromium = await probeChannel(void 0);
514
+ const anyOk = systemChrome.ok || systemEdge.ok || bundledChromium.ok;
515
+ const info = {
516
+ os: `${process.platform} ${process.arch}`,
517
+ node: process.version,
518
+ display: process.platform === "linux" ? process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY ?? null : "n/a",
519
+ playwrightCoreVersion: readPlaywrightCoreVersion(),
520
+ browsersPath,
521
+ bundledChromiumPath,
522
+ bundledChromiumPresent,
523
+ systemChrome,
524
+ systemEdge,
525
+ bundledChromium,
526
+ anyBrowserAvailable: anyOk
527
+ };
528
+ writeArtifact(targetDir, "last-preflight.json", info);
529
+ if (options.json) {
530
+ emitStdout(JSON.stringify(info, null, 2) + "\n");
531
+ process.exit(anyOk ? 0 : PREFLIGHT_NO_BROWSER_EXIT_CODE);
532
+ }
533
+ log4.step(chalk4.bold("verify preflight"));
534
+ log4.info(`OS: ${info.os} \u2022 Node: ${info.node}`);
535
+ log4.info(`Display: ${formatValue(info.display)}`);
536
+ log4.info(`playwright-core: ${formatValue(info.playwrightCoreVersion)}`);
537
+ log4.info(`Browsers cache: ${formatValue(browsersPath)}`);
538
+ log4.info(
539
+ `Bundled Chromium present: ${bundledChromiumPresent ? chalk4.green("yes") : chalk4.yellow("no")}`
540
+ );
541
+ log4.info(probeLine("System Chrome", systemChrome));
542
+ log4.info(probeLine("System Edge", systemEdge));
543
+ log4.info(probeLine("Bundled Chromium", bundledChromium));
544
+ log4.info("");
545
+ if (anyOk) {
546
+ log4.success(chalk4.green("A browser is available \u2014 verify should work."));
547
+ } else {
548
+ log4.warn(
549
+ chalk4.yellow(
550
+ "No working browser found. Run `verify --install-browser`, or install Google Chrome."
551
+ )
552
+ );
553
+ }
554
+ process.exit(anyOk ? 0 : PREFLIGHT_NO_BROWSER_EXIT_CODE);
555
+ }
556
+ async function probeChannel(channel) {
557
+ try {
558
+ const browser = await chromium2.launch({
559
+ headless: true,
560
+ ...channel ? { channel } : {}
561
+ });
562
+ const version = browser.version();
563
+ await browser.close().catch(() => {
564
+ });
565
+ return { ok: true, version };
566
+ } catch (err) {
567
+ return {
568
+ ok: false,
569
+ error: firstLine(err?.message ?? String(err))
570
+ };
571
+ }
572
+ }
573
+ function readPlaywrightCoreVersion() {
574
+ try {
575
+ const require2 = createRequire2(import.meta.url);
576
+ const pkgPath = require2.resolve("playwright-core/package.json");
577
+ return JSON.parse(fs4.readFileSync(pkgPath, "utf8")).version ?? null;
578
+ } catch {
579
+ return null;
580
+ }
581
+ }
582
+ function defaultBrowsersPath() {
583
+ const home = os.homedir();
584
+ if (process.platform === "darwin")
585
+ return path4.join(home, "Library", "Caches", "ms-playwright");
586
+ if (process.platform === "win32")
587
+ return path4.join(home, "AppData", "Local", "ms-playwright");
588
+ return path4.join(home, ".cache", "ms-playwright");
589
+ }
590
+ function probeLine(label, probe) {
591
+ if (probe.ok)
592
+ return `${chalk4.green("\u2713")} ${label}: ${probe.version ?? "available"}`;
593
+ return `${chalk4.yellow("\u2717")} ${label}: ${chalk4.dim(probe.error ?? "unavailable")}`;
594
+ }
595
+ function firstLine(text3) {
596
+ return text3.split("\n")[0];
597
+ }
598
+ function formatValue(value) {
599
+ return value === null || value === void 0 || value === "" ? chalk4.dim("\u2014") : String(value);
600
+ }
601
+
602
+ // src/commands/verify/session.ts
603
+ import chalk5 from "chalk";
604
+
605
+ // src/commands/verify/inject.ts
606
+ var NAV_BINDING = "__csWizardNav";
607
+ var DONE_BINDING = "__csWizardDone";
608
+ function historyHookScript() {
609
+ return `(() => {
610
+ const send = (type) => {
611
+ try { window.${NAV_BINDING}({ type, url: location.href }); } catch {}
612
+ };
613
+ for (const name of ['pushState', 'replaceState']) {
614
+ const orig = history[name];
615
+ history[name] = function () {
616
+ const r = orig.apply(this, arguments);
617
+ send(name);
618
+ return r;
619
+ };
620
+ }
621
+ window.addEventListener('popstate', () => send('popstate'));
622
+ })();`;
623
+ }
624
+ function overlayScript() {
625
+ return `(() => {
626
+ const ID = '__cs-wizard-overlay';
627
+ function mount() {
628
+ if (!document.body || document.getElementById(ID)) return;
629
+ const bar = document.createElement('div');
630
+ bar.id = ID;
631
+ bar.setAttribute('style', [
632
+ 'position:fixed','left:50%','bottom:20px','transform:translateX(-50%)',
633
+ 'z-index:2147483647','background:#1b1b2f','color:#fff',
634
+ 'font:14px/1.4 system-ui,-apple-system,Segoe UI,sans-serif',
635
+ 'padding:12px 16px','border-radius:10px','box-shadow:0 6px 24px rgba(0,0,0,.35)',
636
+ 'display:flex','align-items:center','gap:14px','max-width:90vw'
637
+ ].join(';'));
638
+ const msg = document.createElement('span');
639
+ msg.innerHTML = '<strong>Contentsquare check running.</strong> Click around a few pages, then press Done.';
640
+ const btn = document.createElement('button');
641
+ btn.textContent = 'Done';
642
+ btn.setAttribute('style', [
643
+ 'background:#6c5ce7','color:#fff','border:0','cursor:pointer',
644
+ 'padding:8px 16px','border-radius:8px','font-weight:600','font-size:14px'
645
+ ].join(';'));
646
+ btn.onclick = () => {
647
+ btn.disabled = true;
648
+ btn.textContent = 'Finishing\u2026';
649
+ try { window.${DONE_BINDING}(); } catch {}
650
+ };
651
+ bar.appendChild(msg);
652
+ bar.appendChild(btn);
653
+ document.body.appendChild(bar);
654
+ }
655
+ if (document.readyState === 'loading') {
656
+ document.addEventListener('DOMContentLoaded', mount);
657
+ } else {
658
+ mount();
659
+ }
660
+ })();`;
661
+ }
662
+
663
+ // src/commands/verify/session.ts
664
+ var CS_DOMAIN = String.raw`contentsquare\.(net|com)`;
665
+ var CS_HOST_RE = new RegExp(String.raw`(^|\.)${CS_DOMAIN}$`, "i");
666
+ var CS_DOMAIN_RE = new RegExp(CS_DOMAIN, "i");
667
+ var CSP_RE = /content-security-policy|refused to|violates the following|err_blocked_by_csp|blocked:csp/i;
668
+ var TAG_SCRIPT_RE = /\/uxa\/[^/]+\.js(\?|$)/i;
669
+ async function runVerifySession(browser, url, log4) {
670
+ const navEvents = [];
671
+ const csRequests = [];
672
+ const violations = [];
673
+ let signalDone = () => {
674
+ };
675
+ const doneClicked = new Promise((resolve) => {
676
+ signalDone = resolve;
677
+ });
678
+ let context;
679
+ try {
680
+ context = await browser.newContext();
681
+ await context.exposeBinding(
682
+ NAV_BINDING,
683
+ (_source, data) => {
684
+ navEvents.push({ type: data.type, url: data.url, ts: Date.now() });
685
+ }
686
+ );
687
+ await context.addInitScript(historyHookScript());
688
+ await context.exposeBinding(DONE_BINDING, () => signalDone());
689
+ await context.addInitScript(overlayScript());
690
+ context.on("request", (req) => recordCsRequest(req, csRequests));
691
+ context.on(
692
+ "requestfailed",
693
+ (req) => recordRequestViolation(req, violations)
694
+ );
695
+ const page = await context.newPage();
696
+ page.on(
697
+ "console",
698
+ (msg) => recordConsoleViolation(msg, violations)
699
+ );
700
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
701
+ } catch (err) {
702
+ const message = err?.message ?? String(err);
703
+ if (isBrowserGoneError(message)) {
704
+ log4.error(
705
+ `The browser closed before the page finished loading. (${message})`
706
+ );
707
+ } else {
708
+ log4.error(
709
+ `Could not load ${url}. Is the dev server running? (${message})`
710
+ );
711
+ }
712
+ return closeAndExit(browser, 1);
713
+ }
714
+ printBrowseInstructions(log4);
715
+ await waitForFinish(browser, context, doneClicked);
716
+ return { navEvents, csRequests, violations };
717
+ }
718
+ function recordCsRequest(req, out) {
719
+ const host = safeHost(req.url());
720
+ if (!host || !CS_HOST_RE.test(host)) return;
721
+ const kind = TAG_SCRIPT_RE.test(req.url()) ? "script" : "beacon";
722
+ out.push({ url: req.url(), ts: Date.now(), kind });
723
+ }
724
+ function recordRequestViolation(req, out) {
725
+ const host = safeHost(req.url());
726
+ if (!host || !CS_HOST_RE.test(host)) return;
727
+ const failure = req.failure()?.errorText ?? "";
728
+ if (CSP_RE.test(failure)) {
729
+ out.push({ source: "request", text: `${req.url()} \u2014 ${failure}` });
730
+ }
731
+ }
732
+ function recordConsoleViolation(msg, out) {
733
+ const text3 = msg.text();
734
+ if (CSP_RE.test(text3) && CS_DOMAIN_RE.test(text3)) {
735
+ out.push({ source: "console", text: text3 });
736
+ }
737
+ }
738
+ function printBrowseInstructions(log4) {
739
+ log4.info("");
740
+ log4.step(chalk5.bold("Browse your app now:"));
741
+ log4.info(" 1. Wait a moment for the first page to settle.");
742
+ log4.info(" 2. Click through a few in-app routes (and log in if needed).");
743
+ log4.info(
744
+ ` 3. Click ${chalk5.bold("\u201CDone\u201D")} in the banner (or close the window) to see the report.`
745
+ );
746
+ log4.info("");
747
+ }
748
+ function waitForFinish(browser, context, doneClicked) {
749
+ return new Promise((resolve) => {
750
+ const finish = once(() => {
751
+ browser.close().catch(() => {
752
+ });
753
+ resolve();
754
+ });
755
+ const openPages = new Set(context.pages());
756
+ const watchPage = (p) => {
757
+ p.on("close", () => {
758
+ openPages.delete(p);
759
+ if (openPages.size === 0) finish();
760
+ });
761
+ };
762
+ context.pages().forEach(watchPage);
763
+ context.on("page", (p) => {
764
+ openPages.add(p);
765
+ watchPage(p);
766
+ });
767
+ doneClicked.then(finish);
768
+ browser.on("disconnected", finish);
769
+ context.on("close", finish);
770
+ process.once("SIGINT", finish);
771
+ });
772
+ }
773
+ function safeHost(u) {
774
+ try {
775
+ return new URL(u).hostname;
776
+ } catch {
777
+ return "";
778
+ }
779
+ }
780
+ function once(fn) {
781
+ let called = false;
782
+ return () => {
783
+ if (called) return;
784
+ called = true;
785
+ fn();
786
+ };
787
+ }
788
+
789
+ // src/commands/verify/analyze.ts
790
+ var NAV_BEACON_WINDOW_MS = 6e3;
791
+ var NAV_DEDUPE_WINDOW_MS = 400;
792
+ var SPA_TRACKING_RECOMMENDATION = 'Some in-app route changes did not fire a pageview. If this is a single-page app, enable "Tracking URL Changes" in the Contentsquare app (Project and Users \u2192 Tracking URL Changes).';
793
+ function analyze(input) {
794
+ const { url, projectId, tagId, navEvents, csRequests, violations } = input;
795
+ const scripts = csRequests.filter((r) => r.kind === "script");
796
+ const beacons = csRequests.filter((r) => r.kind === "beacon");
797
+ const tagScriptLoaded = scripts.length > 0;
798
+ const tagIdMismatch = tagScriptLoaded && !scripts.some((s) => s.url.includes(tagId));
799
+ const navs = dropHydrationNav(collapseDuplicateNavs(navEvents), url);
800
+ const firstNavTs = navs[0]?.ts ?? Infinity;
801
+ const initialPageview = beacons.some((b) => b.ts <= firstNavTs);
802
+ const navigations = navs.map((nav) => ({
803
+ url: nav.url,
804
+ type: nav.type,
805
+ pageviewFired: firedPageviewAfter(beacons, nav.ts)
806
+ }));
807
+ const navigationsWithPageview = navigations.filter(
808
+ (n) => n.pageviewFired
809
+ ).length;
810
+ const cspViolations = dedupe(violations, (v) => v.text);
811
+ const { result, recommendation } = gradeResult({
812
+ tagScriptLoaded,
813
+ initialPageview,
814
+ cspViolations,
815
+ navigations,
816
+ navigationsWithPageview
817
+ });
818
+ return {
819
+ url,
820
+ projectId,
821
+ tagId,
822
+ tagScriptLoaded,
823
+ tagIdMismatch,
824
+ initialPageview,
825
+ navigations,
826
+ navigationsWithPageview,
827
+ cspViolations,
828
+ result,
829
+ recommendation
830
+ };
831
+ }
832
+ function collapseDuplicateNavs(navEvents) {
833
+ const navs = [];
834
+ for (const ev of navEvents) {
835
+ const prev = navs[navs.length - 1];
836
+ if (prev && prev.url === ev.url && ev.ts - prev.ts < NAV_DEDUPE_WINDOW_MS)
837
+ continue;
838
+ navs.push(ev);
839
+ }
840
+ return navs;
841
+ }
842
+ function dropHydrationNav(navs, landingUrl) {
843
+ const sameUrl = (a, b) => a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
844
+ return navs.length > 0 && sameUrl(navs[0].url, landingUrl) ? navs.slice(1) : navs;
845
+ }
846
+ function firedPageviewAfter(beacons, navTs) {
847
+ return beacons.some(
848
+ (b) => b.ts > navTs && b.ts <= navTs + NAV_BEACON_WINDOW_MS
849
+ );
850
+ }
851
+ function gradeResult(checks) {
852
+ const {
853
+ tagScriptLoaded,
854
+ initialPageview,
855
+ cspViolations,
856
+ navigations,
857
+ navigationsWithPageview
858
+ } = checks;
859
+ if (!tagScriptLoaded || !initialPageview)
860
+ return { result: "fail", recommendation: null };
861
+ if (cspViolations.length > 0) return { result: "fail", recommendation: null };
862
+ if (navigations.length > 0 && navigationsWithPageview < navigations.length) {
863
+ return {
864
+ result: "pass-with-recommendation",
865
+ recommendation: SPA_TRACKING_RECOMMENDATION
866
+ };
867
+ }
868
+ return { result: "pass", recommendation: null };
869
+ }
870
+ function dedupe(items, key) {
871
+ const seen = /* @__PURE__ */ new Set();
872
+ const out = [];
873
+ for (const item of items) {
874
+ const k = key(item);
875
+ if (seen.has(k)) continue;
876
+ seen.add(k);
877
+ out.push(item);
878
+ }
879
+ return out;
880
+ }
881
+
882
+ // src/commands/verify/report.ts
883
+ import chalk6 from "chalk";
884
+ var MAX_CSP_VIOLATIONS_SHOWN = 8;
885
+ function printHumanReport(r, log4) {
886
+ const ok = chalk6.green("\u2713");
887
+ const no = chalk6.red("\u2717");
888
+ const warn = chalk6.yellow("!");
889
+ log4.info("");
890
+ log4.step(chalk6.bold("Verification report"));
891
+ log4.info(`${r.tagScriptLoaded ? ok : no} Tag script loaded`);
892
+ if (r.tagIdMismatch) {
893
+ log4.info(
894
+ `${warn} Tag URL uses a different ID than ${chalk6.cyan(r.tagId)} \u2014 check the tag ID used at install.`
895
+ );
896
+ }
897
+ log4.info(`${r.initialPageview ? ok : no} First pageview sent on load`);
898
+ if (r.navigations.length === 0) {
899
+ log4.info(
900
+ `${warn} No in-app route changes were recorded \u2014 navigate a few routes to test SPA coverage.`
901
+ );
902
+ } else {
903
+ const icon = r.navigationsWithPageview === r.navigations.length ? ok : warn;
904
+ log4.info(
905
+ `${icon} Pageviews on navigation: ${r.navigationsWithPageview}/${r.navigations.length}`
906
+ );
907
+ for (const n of r.navigations) {
908
+ log4.info(` ${n.pageviewFired ? ok : no} ${n.type} \u2192 ${n.url}`);
909
+ }
910
+ }
911
+ if (r.cspViolations.length > 0) {
912
+ log4.info(`${no} CSP violations (${r.cspViolations.length}):`);
913
+ for (const v of r.cspViolations.slice(0, MAX_CSP_VIOLATIONS_SHOWN)) {
914
+ log4.info(` ${chalk6.dim("-")} ${v.text}`);
915
+ }
916
+ } else {
917
+ log4.info(`${ok} No CSP violations detected`);
918
+ }
919
+ log4.info("");
920
+ if (r.result === "pass") {
921
+ log4.success(chalk6.green("All checks passed."));
922
+ } else if (r.result === "pass-with-recommendation") {
923
+ log4.warn(chalk6.yellow("Passed with a recommendation:"));
924
+ log4.info(` ${r.recommendation}`);
925
+ } else {
926
+ log4.error(chalk6.red("Some checks failed \u2014 see above."));
927
+ }
928
+ }
929
+
930
+ // src/commands/verify/index.ts
931
+ async function verify(options) {
932
+ const targetDir = path5.resolve(options.dir);
933
+ const log4 = makeLogger(!!options.json);
934
+ if (options.preflight) {
935
+ await runPreflight(targetDir, options, log4);
936
+ return;
937
+ }
938
+ const tagId = await resolveTagId(options, targetDir);
939
+ const rawProjectId = readState(targetDir)?.projectId;
940
+ const projectId = rawProjectId !== null && rawProjectId !== void 0 ? String(rawProjectId).trim() : null;
941
+ const url = await resolveUrl(options);
942
+ log4.info(`Tag ID: ${chalk7.cyan(tagId)}`);
943
+ log4.info(`Opening: ${chalk7.cyan(url)}`);
944
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
945
+ handleNoDisplay(options, log4, targetDir);
946
+ }
947
+ const browser = await acquireBrowser(options, log4, targetDir);
948
+ const trace = await runVerifySession(browser, url, log4);
949
+ const report = analyze({ url, projectId, tagId, ...trace });
950
+ const reportPath = path5.join(targetDir, ".cs-wizard", "last-report.json");
951
+ fs5.mkdirSync(path5.dirname(reportPath), { recursive: true });
952
+ fs5.writeFileSync(reportPath, JSON.stringify(report, null, 2));
953
+ if (options.json) {
954
+ emitStdout(JSON.stringify(report, null, 2) + "\n");
955
+ return closeAndExit(browser, report.result === "fail" ? 1 : 0);
956
+ }
957
+ printHumanReport(report, log4);
958
+ emitStdout(
959
+ "\n```json cs-wizard-report\n" + JSON.stringify(report) + "\n```\n"
960
+ );
961
+ return closeAndExit(browser, report.result === "fail" ? 1 : 0);
962
+ }
963
+ async function resolveTagId(options, targetDir) {
964
+ const state = readState(targetDir);
965
+ if (options.tagId) {
966
+ if (validateTagId(options.tagId)) {
967
+ fail(
968
+ `--tag-id must be the hashed tag ID (hex, ~13 chars, lowercase), got: ${options.tagId}`
969
+ );
970
+ }
971
+ return normalizeTagId(options.tagId);
972
+ }
973
+ if (state?.tagId) return state.tagId;
974
+ if (options.json) {
975
+ fail(
976
+ "No tag ID found. Run `wizard start` first, or pass --tag-id <hashed>."
977
+ );
978
+ }
979
+ const answer = await clack4.text({
980
+ message: "Enter your Contentsquare tag ID (hashed hex, e.g. 81c677ba742d7):",
981
+ placeholder: "81c677ba742d7",
982
+ validate: (v) => validateTagId(v)
983
+ });
984
+ if (clack4.isCancel(answer)) {
985
+ clack4.cancel("Verification cancelled.");
986
+ process.exit(0);
987
+ }
988
+ return normalizeTagId(answer);
989
+ }
990
+ async function resolveUrl(options) {
991
+ if (options.url) return options.url;
992
+ if (options.json) fail("No URL provided. Pass --url <app-url>.");
993
+ const answer = await clack4.text({
994
+ message: "What URL should I open? (start your dev server first)",
995
+ placeholder: "http://localhost:3000",
996
+ validate: (v) => /^https?:\/\//.test(v.trim()) ? void 0 : "Enter a full URL (http://\u2026)."
997
+ });
998
+ if (clack4.isCancel(answer)) {
999
+ clack4.cancel("Verification cancelled.");
1000
+ process.exit(0);
1001
+ }
1002
+ return answer.trim();
1003
+ }
1004
+
1005
+ // src/commands/start.ts
1006
+ import * as clack5 from "@clack/prompts";
1007
+ import chalk8 from "chalk";
1008
+ import { exec } from "child_process";
1009
+ import path6 from "path";
1010
+ import fs6 from "fs";
1011
+ import { fileURLToPath } from "url";
1012
+ var __dirname2 = path6.dirname(fileURLToPath(import.meta.url));
1013
+ var PACKAGE_ROOT = findPackageRoot(__dirname2);
1014
+ var SKILL_FILE = "wizard-web-tag-install.md";
1015
+ var SKILL_PATH = `.cs-wizard/skills/${SKILL_FILE}`;
1016
+ var SETUP_PROMPT = `Read and follow ${SKILL_PATH} \u2014 read it directly by path, it is gitignored so do not search for it.`;
1017
+ async function start(options) {
1018
+ process.stdout.write(SORCERER + "\n");
1019
+ clack5.intro(chalk8.bold("Contentsquare Wizard \u2014 Start"));
1020
+ const targetDir = path6.resolve(options.dir);
1021
+ const tagId = await resolveTagId2(targetDir, options);
1022
+ clack5.log.info(`Tag ID: ${chalk8.cyan(tagId)}`);
1023
+ const spinner2 = clack5.spinner();
1024
+ spinner2.start("Setting up wizard skill...");
1025
+ const wizardDir = path6.join(targetDir, ".cs-wizard");
1026
+ const skillsDir = path6.join(wizardDir, "skills");
1027
+ fs6.mkdirSync(skillsDir, { recursive: true });
1028
+ fs6.copyFileSync(
1029
+ path6.join(PACKAGE_ROOT, "skills", SKILL_FILE),
1030
+ path6.join(skillsDir, SKILL_FILE)
1031
+ );
1032
+ spinner2.stop(`Copied skill to ${chalk8.cyan(SKILL_PATH)}`);
1033
+ const state = readExistingConfig(targetDir) ?? {};
1034
+ state.tagId = tagId;
1035
+ state.startedAt = (/* @__PURE__ */ new Date()).toISOString();
1036
+ fs6.writeFileSync(
1037
+ path6.join(wizardDir, "state.json"),
1038
+ JSON.stringify(state, null, 2)
1039
+ );
1040
+ ensureGitignore(targetDir);
1041
+ clack5.log.success("Wizard ready!");
1042
+ clack5.log.info("");
1043
+ await showNextSteps();
1044
+ }
1045
+ async function resolveTagId2(targetDir, options) {
1046
+ if (options.tagId) {
1047
+ const err = validateTagId(options.tagId);
1048
+ if (err) {
1049
+ clack5.log.error(`Invalid --tag-id value: ${err}`);
1050
+ process.exit(1);
1051
+ }
1052
+ return normalizeTagId(options.tagId);
1053
+ }
1054
+ const existing = readExistingConfig(targetDir);
1055
+ if (existing?.tagId) {
1056
+ const reuse = await clack5.confirm({
1057
+ message: `Found existing tag ID ${chalk8.cyan(String(existing.tagId))}. Use it?`,
1058
+ initialValue: true
1059
+ });
1060
+ if (clack5.isCancel(reuse)) {
1061
+ clack5.cancel("Setup cancelled.");
1062
+ process.exit(0);
1063
+ }
1064
+ if (reuse) {
1065
+ return String(existing.tagId);
1066
+ }
1067
+ }
1068
+ const response = await clack5.text({
1069
+ message: "Enter your Contentsquare tag ID (hashed hex, e.g. 81c677ba742d7):",
1070
+ placeholder: "81c677ba742d7",
1071
+ validate: (v) => validateTagId(v)
1072
+ });
1073
+ if (clack5.isCancel(response)) {
1074
+ clack5.cancel("Setup cancelled.");
1075
+ process.exit(0);
1076
+ }
1077
+ return normalizeTagId(response);
1078
+ }
1079
+ async function showNextSteps() {
1080
+ clack5.log.step(chalk8.bold("Next steps:"));
1081
+ clack5.log.info(
1082
+ " 1. Open your AI coding agent (Copilot, Cursor, Claude Code, etc.)"
1083
+ );
1084
+ const copied = await copyToClipboard(SETUP_PROMPT);
1085
+ if (copied) {
1086
+ clack5.log.success(
1087
+ ` 2. ${chalk8.bold.green("Prompt copied to clipboard!")} Paste it in your agent to start.`
1088
+ );
1089
+ } else {
1090
+ clack5.log.info(" 2. Tell your agent:");
1091
+ const border = chalk8.gray("\u2500".repeat(40));
1092
+ clack5.log.info(` ${border}`);
1093
+ for (const line of SETUP_PROMPT.split("\n")) {
1094
+ clack5.log.info(` ${chalk8.cyan(line)}`);
1095
+ }
1096
+ clack5.log.info(` ${border}`);
1097
+ }
1098
+ clack5.log.info("");
1099
+ clack5.outro(
1100
+ `Now ask your AI agent: ${chalk8.cyan(`Read and follow ${SKILL_PATH}`)}`
1101
+ );
1102
+ }
1103
+ function readExistingConfig(targetDir) {
1104
+ const stateFile = path6.join(targetDir, ".cs-wizard", "state.json");
1105
+ if (fs6.existsSync(stateFile)) {
1106
+ try {
1107
+ return JSON.parse(fs6.readFileSync(stateFile, "utf-8"));
1108
+ } catch {
1109
+ return null;
1110
+ }
1111
+ }
1112
+ return null;
1113
+ }
1114
+ function ensureGitignore(targetDir) {
1115
+ const gitignorePath = path6.join(targetDir, ".gitignore");
1116
+ const entry = ".cs-wizard/";
1117
+ if (fs6.existsSync(gitignorePath)) {
1118
+ const content = fs6.readFileSync(gitignorePath, "utf-8");
1119
+ if (!content.includes(entry)) {
1120
+ fs6.appendFileSync(
1121
+ gitignorePath,
1122
+ `
1123
+ # Contentsquare wizard (generated)
1124
+ ${entry}
1125
+ `
1126
+ );
1127
+ }
1128
+ } else {
1129
+ fs6.writeFileSync(
1130
+ gitignorePath,
1131
+ `# Contentsquare wizard (generated)
1132
+ ${entry}
1133
+ `
1134
+ );
1135
+ }
1136
+ }
1137
+ function copyToClipboard(text3) {
1138
+ return new Promise((resolve) => {
1139
+ const cmd = process.platform === "win32" ? `echo ${text3.replace(/["&|<>^]/g, "^$&")} | clip` : process.platform === "darwin" ? `echo ${JSON.stringify(text3)} | pbcopy` : `echo ${JSON.stringify(text3)} | xclip -selection clipboard || echo ${JSON.stringify(text3)} | xsel --clipboard --input`;
1140
+ exec(cmd, (err) => resolve(!err));
1141
+ });
1142
+ }
1143
+ function findPackageRoot(dir) {
1144
+ let current = dir;
1145
+ for (let i = 0; i < 5; i++) {
1146
+ if (fs6.existsSync(path6.join(current, "package.json")) && fs6.existsSync(path6.join(current, "skills"))) {
1147
+ return current;
1148
+ }
1149
+ current = path6.dirname(current);
1150
+ }
1151
+ return path6.resolve(dir, "..");
1152
+ }
1153
+
1154
+ // src/cli.ts
1155
+ var packageJson = JSON.parse(
1156
+ readFileSync(
1157
+ fileURLToPath2(new URL("../package.json", import.meta.url)),
1158
+ "utf8"
1159
+ )
1160
+ );
1161
+ var program = new Command();
1162
+ program.name("wizard-cs").description("AI-assisted Contentsquare tag installation and configuration").version(packageJson.version);
1163
+ function registerStart(name) {
1164
+ program.command(name).description(
1165
+ "Set up the wizard skill so your AI agent can install the Contentsquare tag \u2014 verification runs via `verify`"
1166
+ ).option("--dir <path>", "Target directory (defaults to cwd)", process.cwd()).option(
1167
+ "--tag-id <hashed>",
1168
+ "Hashed Contentsquare tag ID (hex, e.g. 81c677ba742d7)"
1169
+ ).action(start);
1170
+ }
1171
+ registerStart("start");
1172
+ registerStart("install");
1173
+ program.command("status").description("Show the configured tag ID and the latest verification result").option("--dir <path>", "Target directory (defaults to cwd)", process.cwd()).action(status);
1174
+ program.command("verify").description(
1175
+ "Open your app in a real browser and verify the tag, CSP, and pageview coverage in one session"
1176
+ ).option("--dir <path>", "Project directory (defaults to cwd)", process.cwd()).option("--url <url>", "App URL to open (skips the prompt)").option(
1177
+ "--tag-id <hashed>",
1178
+ "Hashed Contentsquare tag ID (hex, e.g. 81c677ba742d7)"
1179
+ ).option(
1180
+ "--install-browser",
1181
+ "Auto-download the Chromium engine if no browser is found"
1182
+ ).option(
1183
+ "--preflight",
1184
+ "Check browser availability and environment, then exit (diagnostics)"
1185
+ ).option("--json", "Emit the report as JSON to stdout (for agents)").action(verify);
1186
+ program.parse();
1187
+ //# sourceMappingURL=cli.js.map