@holdyapp/install 0.2.0 → 0.4.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.
Files changed (2) hide show
  1. package/dist/cli.js +420 -13
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -2,9 +2,9 @@
2
2
  "use strict";
3
3
 
4
4
  // src/cli.ts
5
- var import_node_fs3 = require("node:fs");
6
- var import_node_readline = require("node:readline");
7
- var import_node_path3 = require("node:path");
5
+ var import_node_fs5 = require("node:fs");
6
+ var import_node_readline2 = require("node:readline");
7
+ var import_node_path5 = require("node:path");
8
8
 
9
9
  // src/detect.ts
10
10
  var import_node_fs = require("node:fs");
@@ -235,7 +235,7 @@ function injectNextPages(file, opts) {
235
235
  if (typeof window !== 'undefined' && !window.__holdyInit) { initHoldy({ apiKey: '${opts.apiKey}', ${opts.accountId ? `accountKey: '${opts.accountId}', ` : ""}autoCapture: ${opts.autoCapture} }); (window as unknown as { __holdyInit?: boolean }).__holdyInit = true }`
236
236
  );
237
237
  (0, import_node_fs2.writeFileSync)(file, patched);
238
- return { ok: true, modified: true, file, message: `Patched ${displayPath(file)} \xB7 rode npm install @holdyapp/sdk` };
238
+ return { ok: true, modified: true, file, message: `Patched ${displayPath(file)} \xB7 rode npm install https://holdy.chat/sdk/holdyapp-sdk-1.5.0.tgz` };
239
239
  }
240
240
  function injectViteReact(file, opts) {
241
241
  if (!file || !(0, import_node_fs2.existsSync)(file)) {
@@ -256,7 +256,7 @@ initHoldy({ apiKey: '${opts.apiKey}', ${opts.accountId ? `accountKey: '${opts.ac
256
256
 
257
257
  ${original}`;
258
258
  (0, import_node_fs2.writeFileSync)(file, patched);
259
- return { ok: true, modified: true, file, message: `Patched ${displayPath(file)} \xB7 rode npm install @holdyapp/sdk` };
259
+ return { ok: true, modified: true, file, message: `Patched ${displayPath(file)} \xB7 rode npm install https://holdy.chat/sdk/holdyapp-sdk-1.5.0.tgz` };
260
260
  }
261
261
  function injectViteVue(file, opts) {
262
262
  return injectViteReact(file, opts);
@@ -278,7 +278,7 @@ export default defineNuxtPlugin(() => {
278
278
  })
279
279
  `;
280
280
  (0, import_node_fs2.writeFileSync)(file, content);
281
- return { ok: true, modified: true, file, message: `Criado ${displayPath(file)} \xB7 rode npm install @holdyapp/sdk` };
281
+ return { ok: true, modified: true, file, message: `Criado ${displayPath(file)} \xB7 rode npm install https://holdy.chat/sdk/holdyapp-sdk-1.5.0.tgz` };
282
282
  }
283
283
  function injectRemix(file, opts) {
284
284
  if (!file || !(0, import_node_fs2.existsSync)(file)) {
@@ -381,8 +381,362 @@ function displayPath(p) {
381
381
  return p.replace(process.cwd() + "/", "");
382
382
  }
383
383
 
384
+ // src/instrument.ts
385
+ var import_node_fs4 = require("node:fs");
386
+ var import_node_path4 = require("node:path");
387
+ var import_node_readline = require("node:readline");
388
+
389
+ // src/scan.ts
390
+ var import_node_fs3 = require("node:fs");
391
+ var import_node_path3 = require("node:path");
392
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
393
+ "node_modules",
394
+ ".git",
395
+ ".next",
396
+ ".nuxt",
397
+ ".svelte-kit",
398
+ ".turbo",
399
+ ".vercel",
400
+ "dist",
401
+ "build",
402
+ "out",
403
+ "coverage",
404
+ "public",
405
+ "static",
406
+ "vendor",
407
+ ".cache",
408
+ "storybook-static",
409
+ "__snapshots__",
410
+ ".venv",
411
+ "target"
412
+ ]);
413
+ var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".vue", ".svelte", ".astro"];
414
+ var SKIP_FILE = /\.(test|spec|stories|d)\.[tj]sx?$/;
415
+ var SKIP_CONFIG = /(^|\/)(next|vite|tailwind|postcss|eslint|jest|vitest|rollup|webpack|astro|nuxt|svelte)\.config\./;
416
+ var MAX_FILE_BYTES = 2e5;
417
+ function scanProject(cwd, options = {}) {
418
+ const maxFiles = options.maxFiles ?? 140;
419
+ const maxCharsPerFile = options.maxCharsPerFile ?? 2400;
420
+ const maxTotalChars = options.maxTotalChars ?? 22e4;
421
+ const candidates = [];
422
+ walk(cwd, cwd, candidates, 0);
423
+ candidates.sort((a, b) => b.score - a.score);
424
+ const files = [];
425
+ let total = 0;
426
+ for (const candidate of balanceByKind(candidates)) {
427
+ if (files.length >= maxFiles || total >= maxTotalChars) break;
428
+ let source;
429
+ try {
430
+ source = (0, import_node_fs3.readFileSync)(candidate.abs, "utf8");
431
+ } catch {
432
+ continue;
433
+ }
434
+ const excerpt = source.length > maxCharsPerFile ? `${source.slice(0, maxCharsPerFile)}
435
+ \u2026 (arquivo truncado)` : source;
436
+ if (excerpt.trim().length < 40) continue;
437
+ files.push({ path: candidate.rel, kind: classify(candidate.rel), excerpt });
438
+ total += excerpt.length;
439
+ }
440
+ return files;
441
+ }
442
+ function balanceByKind(candidates) {
443
+ const buckets = /* @__PURE__ */ new Map();
444
+ for (const candidate of candidates) {
445
+ const kind = classify(candidate.rel);
446
+ const bucket = buckets.get(kind) ?? [];
447
+ bucket.push(candidate);
448
+ buckets.set(kind, bucket);
449
+ }
450
+ const order = ["page", "component", "api", "server", "route", "other"];
451
+ const lists = order.map((kind) => buckets.get(kind) ?? []).filter((l) => l.length > 0);
452
+ const out = [];
453
+ let index = 0;
454
+ let remaining = candidates.length;
455
+ while (remaining > 0) {
456
+ let took = 0;
457
+ for (const list of lists) {
458
+ if (index < list.length) {
459
+ out.push(list[index]);
460
+ took++;
461
+ }
462
+ }
463
+ if (took === 0) break;
464
+ remaining -= took;
465
+ index++;
466
+ }
467
+ return out;
468
+ }
469
+ function walk(dir, root, out, depth) {
470
+ if (depth > 8) return;
471
+ let entries;
472
+ try {
473
+ entries = (0, import_node_fs3.readdirSync)(dir);
474
+ } catch {
475
+ return;
476
+ }
477
+ for (const entry of entries) {
478
+ if (entry.startsWith(".") && entry !== ".") continue;
479
+ const abs = (0, import_node_path3.join)(dir, entry);
480
+ let stats;
481
+ try {
482
+ stats = (0, import_node_fs3.statSync)(abs);
483
+ } catch {
484
+ continue;
485
+ }
486
+ if (stats.isDirectory()) {
487
+ if (SKIP_DIRS.has(entry)) continue;
488
+ walk(abs, root, out, depth + 1);
489
+ continue;
490
+ }
491
+ if (!EXTENSIONS.some((ext) => entry.endsWith(ext))) continue;
492
+ if (SKIP_FILE.test(entry)) continue;
493
+ if (stats.size > MAX_FILE_BYTES) continue;
494
+ const rel = (0, import_node_path3.relative)(root, abs).split(import_node_path3.sep).join("/");
495
+ if (SKIP_CONFIG.test(rel)) continue;
496
+ out.push({ abs, rel, score: score(rel, stats.size) });
497
+ }
498
+ }
499
+ function score(rel, size) {
500
+ let value = 0;
501
+ const lower = rel.toLowerCase();
502
+ if (/(^|\/)(app|pages|routes|src\/app|src\/pages)\//.test(lower)) value += 40;
503
+ if (/(^|\/)(actions|api|server|handlers|controllers|services|mutations)\//.test(lower)) value += 35;
504
+ if (/(^|\/)(components|features|views|screens|modules)\//.test(lower)) value += 20;
505
+ if (/(^|\/)(lib|utils|helpers|hooks|types|styles|constants)\//.test(lower)) value -= 15;
506
+ if (/(page|route|layout|form|checkout|billing|signup|onboard|invite|create|new|edit|settings)/.test(lower)) value += 18;
507
+ if (/(icon|logo|svg|theme|i18n|locale|messages|fixture|mock|seed)/.test(lower)) value -= 25;
508
+ const depth = lower.split("/").length;
509
+ value -= depth * 2;
510
+ if (size < 300) value -= 10;
511
+ return value;
512
+ }
513
+ function classify(rel) {
514
+ const lower = rel.toLowerCase();
515
+ if (/page\.[tj]sx?$/.test(lower)) return "page";
516
+ if (/route\.[tj]s$/.test(lower)) return "api";
517
+ if (/(^|\/)api\//.test(lower)) return "api";
518
+ if (/(^|\/)(actions|server|handlers|controllers|services|mutations)\//.test(lower)) return "server";
519
+ if (/(^|\/)(pages|views|screens)\//.test(lower)) return "page";
520
+ if (/(^|\/)routes\//.test(lower)) return "route";
521
+ if (/(^|\/)(components|features|modules)\//.test(lower)) return "component";
522
+ return "other";
523
+ }
524
+
525
+ // src/instrument.ts
526
+ async function runInstrument(opts) {
527
+ const { ui } = opts;
528
+ ui.step("i", "Lendo o c\xF3digo do projeto\u2026");
529
+ const files = scanProject(opts.cwd);
530
+ if (files.length === 0) {
531
+ ui.error("Nenhum arquivo de produto encontrado pra analisar.");
532
+ return 3;
533
+ }
534
+ ui.info(`${files.length} arquivos enviados pra an\xE1lise`);
535
+ ui.step("ii", "Pedindo o plano de instrumenta\xE7\xE3o \xE0 Holdy\u2026");
536
+ let plan;
537
+ try {
538
+ plan = await requestPlan(opts, files);
539
+ } catch (err) {
540
+ ui.error(err instanceof Error ? err.message : "Falha ao falar com a Holdy");
541
+ return 4;
542
+ }
543
+ if (plan.events.length === 0) {
544
+ ui.warn("O planner n\xE3o encontrou ponto de funil claro neste c\xF3digo.");
545
+ if (plan.summary) ui.info(plan.summary);
546
+ return 0;
547
+ }
548
+ console.log();
549
+ console.log(` ${ui.bold(`${plan.events.length} eventos propostos`)}`);
550
+ console.log(` ${ui.dim(plan.summary)}`);
551
+ console.log();
552
+ for (const event of plan.events) {
553
+ const props = event.properties.map((p) => p.name).join(", ");
554
+ console.log(` ${ui.cyan(event.event)} ${ui.dim(`\xB7 ${event.lifecycle_phase} \xB7 confian\xE7a ${event.confidence}`)}`);
555
+ console.log(` ${event.purpose}`);
556
+ console.log(` ${ui.dim(event.file)}${props ? ui.dim(` \xB7 props: ${props}`) : ""}`);
557
+ }
558
+ console.log();
559
+ const { patches, skipped } = resolvePatches(opts.cwd, plan.events);
560
+ for (const skip of skipped) {
561
+ ui.warn(`${skip.event.event} \xB7 ${skip.reason}`);
562
+ console.log(` ${ui.dim("insira manualmente em")} ${skip.event.file}:`);
563
+ console.log(` ${skip.event.snippet}`);
564
+ }
565
+ if (opts.dryRun) {
566
+ console.log();
567
+ ui.info("--dry-run: nada foi escrito.");
568
+ return 0;
569
+ }
570
+ if (patches.length === 0) {
571
+ ui.warn("Nenhuma inser\xE7\xE3o autom\xE1tica poss\xEDvel \u2014 s\xF3 os snippets manuais acima.");
572
+ return 0;
573
+ }
574
+ console.log();
575
+ console.log(` ${ui.bold("Diff proposto")}`);
576
+ console.log();
577
+ for (const patch of patches) {
578
+ printDiff(patch, ui);
579
+ }
580
+ if (!opts.yes) {
581
+ const answer = await confirm(
582
+ ` Aplicar ${patches.length} ${patches.length === 1 ? "inser\xE7\xE3o" : "inser\xE7\xF5es"}? [y/N] `
583
+ );
584
+ if (!answer) {
585
+ ui.info("Cancelado \xB7 nada foi escrito.");
586
+ return 0;
587
+ }
588
+ }
589
+ const written = applyPatches(patches);
590
+ console.log();
591
+ ui.success(
592
+ `${patches.length} ${patches.length === 1 ? "evento inserido" : "eventos inseridos"} em ${written.length} ${written.length === 1 ? "arquivo" : "arquivos"}`
593
+ );
594
+ for (const file of written) ui.info(file);
595
+ printMetrics(plan.metrics, ui);
596
+ console.log();
597
+ ui.info("Os eventos se registram sozinhos na taxonomia no primeiro emit \u2014 n\xE3o precisa declarar nada na Holdy.");
598
+ ui.info("Rode o app, use o produto e acompanhe em holdy.chat/onboarding.");
599
+ return 0;
600
+ }
601
+ function printMetrics(metrics, ui) {
602
+ if (!metrics || metrics.length === 0) return;
603
+ const prontas = metrics.filter((m) => m.ready);
604
+ const faltando = metrics.filter((m) => !m.ready);
605
+ console.log();
606
+ console.log(` ${ui.bold("O que voc\xEA passa a medir")}`);
607
+ console.log();
608
+ for (const m of prontas) {
609
+ const extra = m.needs_choice ? ui.dim(` \xB7 ${m.needs_choice}`) : "";
610
+ console.log(` ${ui.green("\u2713")} ${m.label}${extra}`);
611
+ }
612
+ for (const m of faltando) {
613
+ console.log(` ${ui.dim("\xB7")} ${ui.dim(`${m.label} \u2014 falta evento de fase ${m.missing.join(" e ")}`)}`);
614
+ }
615
+ }
616
+ async function requestPlan(opts, files) {
617
+ const url = `${opts.baseUrl.replace(/\/$/, "")}/instrumentation/plan`;
618
+ const response = await fetch(url, {
619
+ method: "POST",
620
+ headers: {
621
+ "Content-Type": "application/json",
622
+ Authorization: `Bearer ${opts.apiKey}`
623
+ },
624
+ body: JSON.stringify({
625
+ framework: opts.framework,
626
+ sdk_style: opts.sdkStyle,
627
+ max_events: opts.maxEvents,
628
+ files
629
+ })
630
+ });
631
+ if (!response.ok) {
632
+ const problem = await response.json().catch(() => null);
633
+ const reason = problem?.detail ?? problem?.title ?? `HTTP ${response.status}`;
634
+ if (response.status === 403) {
635
+ throw new Error(`${reason} \xB7 gere uma key secreta em holdy.chat/settings/api`);
636
+ }
637
+ throw new Error(reason);
638
+ }
639
+ return await response.json();
640
+ }
641
+ function resolvePatches(cwd, events) {
642
+ const patches = [];
643
+ const skipped = [];
644
+ const cache = /* @__PURE__ */ new Map();
645
+ for (const event of events) {
646
+ const absolute = (0, import_node_path4.join)(cwd, event.file);
647
+ let source = cache.get(absolute);
648
+ if (source === void 0) {
649
+ try {
650
+ source = (0, import_node_fs4.readFileSync)(absolute, "utf8");
651
+ } catch {
652
+ skipped.push({ event, reason: "arquivo n\xE3o encontrado no disco" });
653
+ continue;
654
+ }
655
+ cache.set(absolute, source);
656
+ }
657
+ const first = source.indexOf(event.anchor);
658
+ if (first === -1) {
659
+ skipped.push({ event, reason: "\xE2ncora n\xE3o encontrada no arquivo" });
660
+ continue;
661
+ }
662
+ if (source.indexOf(event.anchor, first + 1) !== -1) {
663
+ skipped.push({ event, reason: "\xE2ncora aparece mais de uma vez \xB7 amb\xEDguo" });
664
+ continue;
665
+ }
666
+ if (source.includes(`'${event.event}'`) || source.includes(`"${event.event}"`)) {
667
+ skipped.push({ event, reason: "evento j\xE1 instrumentado neste arquivo" });
668
+ continue;
669
+ }
670
+ const lines = source.split("\n");
671
+ const anchorLine = lineOf(source, first);
672
+ const anchorEndLine = lineOf(source, first + event.anchor.length - 1);
673
+ const targetLine = event.position === "after" ? anchorEndLine + 1 : anchorLine;
674
+ const indent = indentOf(lines[event.position === "after" ? anchorEndLine : anchorLine] ?? "");
675
+ const insertion = event.snippet.split("\n").map((line) => line.trim() ? indent + line.trim() : line).join("\n");
676
+ patches.push({
677
+ event,
678
+ file: event.file,
679
+ absolute,
680
+ index: targetLine,
681
+ insertion,
682
+ previewLine: targetLine,
683
+ context: lines.slice(Math.max(0, targetLine - 3), targetLine + 2)
684
+ });
685
+ }
686
+ return { patches, skipped };
687
+ }
688
+ function applyPatches(patches) {
689
+ const byFile = /* @__PURE__ */ new Map();
690
+ for (const patch of patches) {
691
+ const list = byFile.get(patch.absolute) ?? [];
692
+ list.push(patch);
693
+ byFile.set(patch.absolute, list);
694
+ }
695
+ const written = [];
696
+ for (const [absolute, filePatches] of byFile) {
697
+ const lines = (0, import_node_fs4.readFileSync)(absolute, "utf8").split("\n");
698
+ filePatches.sort((a, b) => b.index - a.index);
699
+ for (const patch of filePatches) {
700
+ lines.splice(patch.index, 0, patch.insertion);
701
+ }
702
+ (0, import_node_fs4.writeFileSync)(absolute, lines.join("\n"), "utf8");
703
+ written.push(filePatches[0].file);
704
+ }
705
+ return written;
706
+ }
707
+ function printDiff(patch, ui) {
708
+ console.log(` ${ui.dim(`${patch.file}:${patch.previewLine + 1}`)} ${ui.cyan(patch.event.event)}`);
709
+ const before = patch.context.slice(0, 3);
710
+ const after = patch.context.slice(3);
711
+ for (const line of before) console.log(` ${ui.dim(` ${line}`)}`);
712
+ console.log(` ${ui.green(`+ ${patch.insertion.trim()}`)}`);
713
+ for (const line of after) console.log(` ${ui.dim(` ${line}`)}`);
714
+ console.log();
715
+ }
716
+ function lineOf(source, index) {
717
+ let line = 0;
718
+ for (let i = 0; i < index; i++) {
719
+ if (source.charCodeAt(i) === 10) line++;
720
+ }
721
+ return line;
722
+ }
723
+ function indentOf(line) {
724
+ const match = line.match(/^[ \t]*/);
725
+ return match ? match[0] : "";
726
+ }
727
+ function confirm(question) {
728
+ return new Promise((resolve) => {
729
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
730
+ rl.question(question, (answer) => {
731
+ rl.close();
732
+ resolve(/^(s|sim|y|yes)$/i.test(answer.trim()));
733
+ });
734
+ });
735
+ }
736
+
384
737
  // src/cli.ts
385
- var VERSION = "0.1.0";
738
+ var VERSION = "0.4.0";
739
+ var DEFAULT_API_URL = "https://holdy.chat/api/v1";
386
740
  async function main() {
387
741
  const argv = process.argv.slice(2);
388
742
  const cwd = process.cwd();
@@ -431,19 +785,47 @@ async function main() {
431
785
  }
432
786
  process.exit(2);
433
787
  }
434
- if (detection.usePackage) {
788
+ if (detection.usePackage && detection.framework !== "next-app") {
435
789
  log.step("3", "Falta instalar o pacote npm:");
436
790
  console.log();
437
- console.log(" npm install @holdyapp/sdk");
791
+ console.log(" npm install https://holdy.chat/sdk/holdyapp-sdk-1.5.0.tgz");
438
792
  console.log();
439
793
  } else {
440
794
  log.step("3", "Sem npm install necess\xE1rio \xB7 script tag via CDN.");
441
795
  }
442
- log.step("4", 'Pronto! Inicie o app e abra o widget de status em holdy.chat \xB7 voc\xEA deve ver "conectado" em segundos.');
796
+ if (args.instrument) {
797
+ console.log();
798
+ log.step("4", "Instrumentando eventos de produto\u2026");
799
+ const code = await runInstrument({
800
+ cwd,
801
+ apiKey,
802
+ baseUrl: args.apiUrl,
803
+ framework: detection.framework,
804
+ sdkStyle: detection.usePackage ? "npm" : "script",
805
+ maxEvents: args.maxEvents,
806
+ dryRun: args.dryRun,
807
+ yes: args.yes,
808
+ ui: { ...log, bold, dim, green, yellow, cyan }
809
+ });
810
+ if (code !== 0) process.exit(code);
811
+ console.log();
812
+ log.step("5", "Pronto! Suba o app, use o produto e veja os eventos chegando em holdy.chat.");
813
+ process.exit(0);
814
+ }
815
+ log.step("4", "Pronto! Inicie o app e abra holdy.chat/onboarding \xB7 o primeiro evento aparece em segundos.");
443
816
  process.exit(0);
444
817
  }
445
818
  function parseArgs(argv) {
446
- const out = { apiKey: null, accountId: void 0, autoCapture: true };
819
+ const out = {
820
+ apiKey: null,
821
+ accountId: void 0,
822
+ autoCapture: true,
823
+ instrument: false,
824
+ dryRun: false,
825
+ yes: false,
826
+ maxEvents: 12,
827
+ apiUrl: process.env.HOLDY_API_URL ?? DEFAULT_API_URL
828
+ };
447
829
  for (let i = 0; i < argv.length; i++) {
448
830
  const a = argv[i];
449
831
  if (a === "--account" || a === "--account-id") {
@@ -452,6 +834,18 @@ function parseArgs(argv) {
452
834
  out.autoCapture = false;
453
835
  } else if (a === "--auto-capture") {
454
836
  out.autoCapture = true;
837
+ } else if (a === "--instrument") {
838
+ out.instrument = true;
839
+ } else if (a === "--dry-run") {
840
+ out.instrument = true;
841
+ out.dryRun = true;
842
+ } else if (a === "--yes" || a === "-y") {
843
+ out.yes = true;
844
+ } else if (a === "--max-events") {
845
+ const parsed = Number(argv[++i]);
846
+ if (Number.isFinite(parsed)) out.maxEvents = Math.min(20, Math.max(3, Math.trunc(parsed)));
847
+ } else if (a === "--api-url") {
848
+ out.apiUrl = argv[++i] ?? out.apiUrl;
455
849
  } else if (!a.startsWith("--") && !out.apiKey) {
456
850
  out.apiKey = a;
457
851
  }
@@ -463,7 +857,7 @@ function parseArgs(argv) {
463
857
  }
464
858
  function promptApiKey() {
465
859
  return new Promise((resolve) => {
466
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
860
+ const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
467
861
  rl.question(`${dim("API key")} (hol_live_\u2026 ou hol_test_\u2026): `, (answer) => {
468
862
  rl.close();
469
863
  resolve(answer.trim());
@@ -486,6 +880,11 @@ ${bold("Argumentos:")}
486
880
  ${bold("Op\xE7\xF5es:")}
487
881
  --account <id> Chave da account padr\xE3o (vira context.account_external_id)
488
882
  --no-auto-capture Desabilita captura autom\xE1tica de page views
883
+ --instrument L\xEA seu c\xF3digo, prop\xF5e eventos de funil e insere os track()
884
+ --dry-run S\xF3 lista os eventos propostos \xB7 n\xE3o escreve nada (implica --instrument)
885
+ --max-events <n> Teto de eventos propostos \xB7 3 a 20 \xB7 default 12
886
+ --yes, -y Aplica o diff sem perguntar
887
+ --api-url <url> Override da API (default https://holdy.chat/api/v1)
489
888
  --version, -v Mostra vers\xE3o
490
889
  --help, -h Mostra esta mensagem
491
890
 
@@ -496,11 +895,19 @@ ${bold("Frameworks suportados:")}
496
895
  ${bold("Exemplos:")}
497
896
  npx @holdyapp/install hol_live_abc123 --account acme-corp
498
897
  npx @holdyapp/install hol_test_xyz789 --no-auto-capture
898
+ npx @holdyapp/install hol_live_abc123 --instrument
899
+ npx @holdyapp/install hol_live_abc123 --dry-run --max-events 15
900
+
901
+ ${bold("Sobre --instrument:")}
902
+ Manda trechos do seu c\xF3digo pra Holdy, que devolve onde inserir cada track().
903
+ Exige key SECRETA (n\xE3o a p\xFAblica do browser). Voc\xEA v\xEA o diff antes de aplicar,
904
+ e nada \xE9 escrito sem confirma\xE7\xE3o. Pageview e rota j\xE1 s\xE3o autom\xE1ticos \u2014 o
905
+ planner s\xF3 prop\xF5e momentos em que a conta avan\xE7a no produto.
499
906
  `);
500
907
  }
501
908
  function readPackageName() {
502
909
  try {
503
- const pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(__dirname, "..", "package.json"), "utf8"));
910
+ const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(__dirname, "..", "package.json"), "utf8"));
504
911
  return pkg.description ?? null;
505
912
  } catch {
506
913
  return null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@holdyapp/install",
3
- "version": "0.2.0",
4
- "description": "Holdy install CLI \u00b7 detects your framework and wires the SDK in 1 step",
3
+ "version": "0.4.0",
4
+ "description": "Holdy install CLI · detects your framework, wires the SDK and instruments product events",
5
5
  "license": "MIT",
6
6
  "homepage": "https://holdy.chat",
7
7
  "repository": {