@holdyapp/install 0.1.0 → 0.3.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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +414 -23
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -64,7 +64,7 @@ Re-rodar o CLI no mesmo projeto = no-op. Os patches detectam o marker `holdy-ins
64
64
 
65
65
  npm install @holdyapp/sdk
66
66
 
67
- [4] Pronto! Inicie o app e abra o widget de status em app.holdy.com.br
67
+ [4] Pronto! Inicie o app e abra o widget de status em holdy.chat
68
68
  ```
69
69
 
70
70
  ## Licença
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");
@@ -82,7 +82,7 @@ function pick(cwd, candidates) {
82
82
  var import_node_fs2 = require("node:fs");
83
83
  var import_node_path2 = require("node:path");
84
84
  var HOLDY_MARKER = "holdy-installed-by-cli";
85
- var CDN_URL = "https://app.holdy.com.br/sdk/v0/holdy.iife.js";
85
+ var CDN_URL = "https://holdy.chat/sdk/v1/holdy.js";
86
86
  function inject(detection, opts) {
87
87
  switch (detection.framework) {
88
88
  case "next-app":
@@ -189,7 +189,7 @@ export function HoldyScript() {
189
189
  s.onload = () => {
190
190
  window.Holdy?.init({
191
191
  apiKey: '${opts.apiKey}',
192
- ${opts.accountId ? `accountId: '${opts.accountId}',
192
+ ${opts.accountId ? `accountKey: '${opts.accountId}',
193
193
  ` : ""}autoCapture: ${opts.autoCapture},
194
194
  })
195
195
  }
@@ -228,11 +228,11 @@ function injectNextPages(file, opts) {
228
228
  if (original.includes(HOLDY_MARKER) || original.includes("@holdyapp/sdk")) {
229
229
  return { ok: true, modified: false, file, message: "Holdy j\xE1 presente no _app \xB7 skip" };
230
230
  }
231
- let patched = addImport(original, `import { Holdy } from '@holdyapp/sdk' // ${HOLDY_MARKER}`);
231
+ let patched = addImport(original, `import { initHoldy } from '@holdyapp/sdk/web' // ${HOLDY_MARKER}`);
232
232
  patched = patched.replace(
233
233
  /export default function (\w+)\([^)]*\)\s*\{/,
234
234
  (match) => `${match}
235
- if (typeof window !== 'undefined' && !window.__holdyInit) { Holdy.init({ apiKey: '${opts.apiKey}', ${opts.accountId ? `accountId: '${opts.accountId}', ` : ""}autoCapture: ${opts.autoCapture} }); (window as unknown as { __holdyInit?: boolean }).__holdyInit = true }`
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
238
  return { ok: true, modified: true, file, message: `Patched ${displayPath(file)} \xB7 rode npm install @holdyapp/sdk` };
@@ -251,8 +251,8 @@ function injectViteReact(file, opts) {
251
251
  if (original.includes(HOLDY_MARKER) || original.includes("@holdyapp/sdk")) {
252
252
  return { ok: true, modified: false, file, message: "Holdy j\xE1 presente no entry \xB7 skip" };
253
253
  }
254
- const patched = `import { Holdy } from '@holdyapp/sdk' // ${HOLDY_MARKER}
255
- Holdy.init({ apiKey: '${opts.apiKey}', ${opts.accountId ? `accountId: '${opts.accountId}', ` : ""}autoCapture: ${opts.autoCapture} })
254
+ const patched = `import { initHoldy } from '@holdyapp/sdk/web' // ${HOLDY_MARKER}
255
+ initHoldy({ apiKey: '${opts.apiKey}', ${opts.accountId ? `accountKey: '${opts.accountId}', ` : ""}autoCapture: ${opts.autoCapture} })
256
256
 
257
257
  ${original}`;
258
258
  (0, import_node_fs2.writeFileSync)(file, patched);
@@ -267,12 +267,12 @@ function injectNuxt(file, opts) {
267
267
  }
268
268
  ensureDir(file);
269
269
  const content = `// ${HOLDY_MARKER}
270
- import { Holdy } from '@holdyapp/sdk'
270
+ import { initHoldy } from '@holdyapp/sdk/web'
271
271
 
272
272
  export default defineNuxtPlugin(() => {
273
- Holdy.init({
273
+ initHoldy({
274
274
  apiKey: '${opts.apiKey}',
275
- ${opts.accountId ? `accountId: '${opts.accountId}',
275
+ ${opts.accountId ? `accountKey: '${opts.accountId}',
276
276
  ` : ""}autoCapture: ${opts.autoCapture},
277
277
  })
278
278
  })
@@ -341,7 +341,7 @@ function injectHtmlOrAstro(file, opts) {
341
341
  }
342
342
  function buildScriptSnippet(opts) {
343
343
  const accountLine = opts.accountId ? `
344
- accountId: '${opts.accountId}',` : "";
344
+ accountKey: '${opts.accountId}',` : "";
345
345
  return `<script src="${CDN_URL}"></script>
346
346
  <script>
347
347
  Holdy.init({
@@ -352,10 +352,10 @@ function buildScriptSnippet(opts) {
352
352
  }
353
353
  function buildEsImportSnippet(opts) {
354
354
  const accountLine = opts.accountId ? `
355
- accountId: '${opts.accountId}',` : "";
356
- return `import { Holdy } from '@holdyapp/sdk'
355
+ accountKey: '${opts.accountId}',` : "";
356
+ return `import { initHoldy } from '@holdyapp/sdk/web'
357
357
 
358
- Holdy.init({
358
+ initHoldy({
359
359
  apiKey: '${opts.apiKey}',${accountLine}
360
360
  autoCapture: ${opts.autoCapture},
361
361
  })`;
@@ -381,8 +381,346 @@ 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
+ console.log();
596
+ ui.info("Os eventos se registram sozinhos na taxonomia no primeiro emit \u2014 n\xE3o precisa declarar nada na Holdy.");
597
+ ui.info("Rode o app, use o produto e acompanhe em /setup/captura.");
598
+ return 0;
599
+ }
600
+ async function requestPlan(opts, files) {
601
+ const url = `${opts.baseUrl.replace(/\/$/, "")}/instrumentation/plan`;
602
+ const response = await fetch(url, {
603
+ method: "POST",
604
+ headers: {
605
+ "Content-Type": "application/json",
606
+ Authorization: `Bearer ${opts.apiKey}`
607
+ },
608
+ body: JSON.stringify({
609
+ framework: opts.framework,
610
+ sdk_style: opts.sdkStyle,
611
+ max_events: opts.maxEvents,
612
+ files
613
+ })
614
+ });
615
+ if (!response.ok) {
616
+ const problem = await response.json().catch(() => null);
617
+ const reason = problem?.detail ?? problem?.title ?? `HTTP ${response.status}`;
618
+ if (response.status === 403) {
619
+ throw new Error(`${reason} \xB7 gere uma key secreta em holdy.chat/settings/api`);
620
+ }
621
+ throw new Error(reason);
622
+ }
623
+ return await response.json();
624
+ }
625
+ function resolvePatches(cwd, events) {
626
+ const patches = [];
627
+ const skipped = [];
628
+ const cache = /* @__PURE__ */ new Map();
629
+ for (const event of events) {
630
+ const absolute = (0, import_node_path4.join)(cwd, event.file);
631
+ let source = cache.get(absolute);
632
+ if (source === void 0) {
633
+ try {
634
+ source = (0, import_node_fs4.readFileSync)(absolute, "utf8");
635
+ } catch {
636
+ skipped.push({ event, reason: "arquivo n\xE3o encontrado no disco" });
637
+ continue;
638
+ }
639
+ cache.set(absolute, source);
640
+ }
641
+ const first = source.indexOf(event.anchor);
642
+ if (first === -1) {
643
+ skipped.push({ event, reason: "\xE2ncora n\xE3o encontrada no arquivo" });
644
+ continue;
645
+ }
646
+ if (source.indexOf(event.anchor, first + 1) !== -1) {
647
+ skipped.push({ event, reason: "\xE2ncora aparece mais de uma vez \xB7 amb\xEDguo" });
648
+ continue;
649
+ }
650
+ if (source.includes(`'${event.event}'`) || source.includes(`"${event.event}"`)) {
651
+ skipped.push({ event, reason: "evento j\xE1 instrumentado neste arquivo" });
652
+ continue;
653
+ }
654
+ const lines = source.split("\n");
655
+ const anchorLine = lineOf(source, first);
656
+ const anchorEndLine = lineOf(source, first + event.anchor.length - 1);
657
+ const targetLine = event.position === "after" ? anchorEndLine + 1 : anchorLine;
658
+ const indent = indentOf(lines[event.position === "after" ? anchorEndLine : anchorLine] ?? "");
659
+ const insertion = event.snippet.split("\n").map((line) => line.trim() ? indent + line.trim() : line).join("\n");
660
+ patches.push({
661
+ event,
662
+ file: event.file,
663
+ absolute,
664
+ index: targetLine,
665
+ insertion,
666
+ previewLine: targetLine,
667
+ context: lines.slice(Math.max(0, targetLine - 3), targetLine + 2)
668
+ });
669
+ }
670
+ return { patches, skipped };
671
+ }
672
+ function applyPatches(patches) {
673
+ const byFile = /* @__PURE__ */ new Map();
674
+ for (const patch of patches) {
675
+ const list = byFile.get(patch.absolute) ?? [];
676
+ list.push(patch);
677
+ byFile.set(patch.absolute, list);
678
+ }
679
+ const written = [];
680
+ for (const [absolute, filePatches] of byFile) {
681
+ const lines = (0, import_node_fs4.readFileSync)(absolute, "utf8").split("\n");
682
+ filePatches.sort((a, b) => b.index - a.index);
683
+ for (const patch of filePatches) {
684
+ lines.splice(patch.index, 0, patch.insertion);
685
+ }
686
+ (0, import_node_fs4.writeFileSync)(absolute, lines.join("\n"), "utf8");
687
+ written.push(filePatches[0].file);
688
+ }
689
+ return written;
690
+ }
691
+ function printDiff(patch, ui) {
692
+ console.log(` ${ui.dim(`${patch.file}:${patch.previewLine + 1}`)} ${ui.cyan(patch.event.event)}`);
693
+ const before = patch.context.slice(0, 3);
694
+ const after = patch.context.slice(3);
695
+ for (const line of before) console.log(` ${ui.dim(` ${line}`)}`);
696
+ console.log(` ${ui.green(`+ ${patch.insertion.trim()}`)}`);
697
+ for (const line of after) console.log(` ${ui.dim(` ${line}`)}`);
698
+ console.log();
699
+ }
700
+ function lineOf(source, index) {
701
+ let line = 0;
702
+ for (let i = 0; i < index; i++) {
703
+ if (source.charCodeAt(i) === 10) line++;
704
+ }
705
+ return line;
706
+ }
707
+ function indentOf(line) {
708
+ const match = line.match(/^[ \t]*/);
709
+ return match ? match[0] : "";
710
+ }
711
+ function confirm(question) {
712
+ return new Promise((resolve) => {
713
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
714
+ rl.question(question, (answer) => {
715
+ rl.close();
716
+ resolve(/^(s|sim|y|yes)$/i.test(answer.trim()));
717
+ });
718
+ });
719
+ }
720
+
384
721
  // src/cli.ts
385
- var VERSION = "0.1.0";
722
+ var VERSION = "0.3.0";
723
+ var DEFAULT_API_URL = "https://holdy.chat/api/v1";
386
724
  async function main() {
387
725
  const argv = process.argv.slice(2);
388
726
  const cwd = process.cwd();
@@ -407,7 +745,7 @@ async function main() {
407
745
  apiKey = await promptApiKey();
408
746
  }
409
747
  if (!apiKey || !apiKey.startsWith("hol_")) {
410
- log.error("API key inv\xE1lida. Use formato hol_live_\u2026 ou hol_test_\u2026 \xB7 gere em app.holdy.com.br/settings.");
748
+ log.error("API key inv\xE1lida. Use formato hol_live_\u2026 ou hol_test_\u2026 \xB7 gere em holdy.chat/settings.");
411
749
  process.exit(1);
412
750
  }
413
751
  log.step("2", "Aplicando snippet no(s) arquivo(s)\u2026");
@@ -439,11 +777,39 @@ async function main() {
439
777
  } else {
440
778
  log.step("3", "Sem npm install necess\xE1rio \xB7 script tag via CDN.");
441
779
  }
442
- log.step("4", 'Pronto! Inicie o app e abra o widget de status em app.holdy.com.br \xB7 voc\xEA deve ver "conectado" em segundos.');
780
+ if (args.instrument) {
781
+ console.log();
782
+ log.step("4", "Instrumentando eventos de produto\u2026");
783
+ const code = await runInstrument({
784
+ cwd,
785
+ apiKey,
786
+ baseUrl: args.apiUrl,
787
+ framework: detection.framework,
788
+ sdkStyle: detection.usePackage ? "npm" : "script",
789
+ maxEvents: args.maxEvents,
790
+ dryRun: args.dryRun,
791
+ yes: args.yes,
792
+ ui: { ...log, bold, dim, green, yellow, cyan }
793
+ });
794
+ if (code !== 0) process.exit(code);
795
+ console.log();
796
+ log.step("5", "Pronto! Suba o app, use o produto e veja os eventos chegando em holdy.chat.");
797
+ process.exit(0);
798
+ }
799
+ log.step("4", 'Pronto! Inicie o app e abra o widget de status em holdy.chat \xB7 voc\xEA deve ver "conectado" em segundos.');
443
800
  process.exit(0);
444
801
  }
445
802
  function parseArgs(argv) {
446
- const out = { apiKey: null, accountId: void 0, autoCapture: true };
803
+ const out = {
804
+ apiKey: null,
805
+ accountId: void 0,
806
+ autoCapture: true,
807
+ instrument: false,
808
+ dryRun: false,
809
+ yes: false,
810
+ maxEvents: 12,
811
+ apiUrl: process.env.HOLDY_API_URL ?? DEFAULT_API_URL
812
+ };
447
813
  for (let i = 0; i < argv.length; i++) {
448
814
  const a = argv[i];
449
815
  if (a === "--account" || a === "--account-id") {
@@ -452,6 +818,18 @@ function parseArgs(argv) {
452
818
  out.autoCapture = false;
453
819
  } else if (a === "--auto-capture") {
454
820
  out.autoCapture = true;
821
+ } else if (a === "--instrument") {
822
+ out.instrument = true;
823
+ } else if (a === "--dry-run") {
824
+ out.instrument = true;
825
+ out.dryRun = true;
826
+ } else if (a === "--yes" || a === "-y") {
827
+ out.yes = true;
828
+ } else if (a === "--max-events") {
829
+ const parsed = Number(argv[++i]);
830
+ if (Number.isFinite(parsed)) out.maxEvents = Math.min(20, Math.max(3, Math.trunc(parsed)));
831
+ } else if (a === "--api-url") {
832
+ out.apiUrl = argv[++i] ?? out.apiUrl;
455
833
  } else if (!a.startsWith("--") && !out.apiKey) {
456
834
  out.apiKey = a;
457
835
  }
@@ -463,7 +841,7 @@ function parseArgs(argv) {
463
841
  }
464
842
  function promptApiKey() {
465
843
  return new Promise((resolve) => {
466
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
844
+ const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
467
845
  rl.question(`${dim("API key")} (hol_live_\u2026 ou hol_test_\u2026): `, (answer) => {
468
846
  rl.close();
469
847
  resolve(answer.trim());
@@ -484,8 +862,13 @@ ${bold("Argumentos:")}
484
862
  apiKey Token Holdy (hol_live_\u2026 ou hol_test_\u2026)
485
863
 
486
864
  ${bold("Op\xE7\xF5es:")}
487
- --account <id> Account ID padr\xE3o (vai pro Holdy.init)
865
+ --account <id> Chave da account padr\xE3o (vira context.account_external_id)
488
866
  --no-auto-capture Desabilita captura autom\xE1tica de page views
867
+ --instrument L\xEA seu c\xF3digo, prop\xF5e eventos de funil e insere os track()
868
+ --dry-run S\xF3 lista os eventos propostos \xB7 n\xE3o escreve nada (implica --instrument)
869
+ --max-events <n> Teto de eventos propostos \xB7 3 a 20 \xB7 default 12
870
+ --yes, -y Aplica o diff sem perguntar
871
+ --api-url <url> Override da API (default https://holdy.chat/api/v1)
489
872
  --version, -v Mostra vers\xE3o
490
873
  --help, -h Mostra esta mensagem
491
874
 
@@ -496,11 +879,19 @@ ${bold("Frameworks suportados:")}
496
879
  ${bold("Exemplos:")}
497
880
  npx @holdyapp/install hol_live_abc123 --account acme-corp
498
881
  npx @holdyapp/install hol_test_xyz789 --no-auto-capture
882
+ npx @holdyapp/install hol_live_abc123 --instrument
883
+ npx @holdyapp/install hol_live_abc123 --dry-run --max-events 15
884
+
885
+ ${bold("Sobre --instrument:")}
886
+ Manda trechos do seu c\xF3digo pra Holdy, que devolve onde inserir cada track().
887
+ Exige key SECRETA (n\xE3o a p\xFAblica do browser). Voc\xEA v\xEA o diff antes de aplicar,
888
+ e nada \xE9 escrito sem confirma\xE7\xE3o. Pageview e rota j\xE1 s\xE3o autom\xE1ticos \u2014 o
889
+ planner s\xF3 prop\xF5e momentos em que a conta avan\xE7a no produto.
499
890
  `);
500
891
  }
501
892
  function readPackageName() {
502
893
  try {
503
- const pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(__dirname, "..", "package.json"), "utf8"));
894
+ const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(__dirname, "..", "package.json"), "utf8"));
504
895
  return pkg.description ?? null;
505
896
  } catch {
506
897
  return null;
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@holdyapp/install",
3
- "version": "0.1.0",
4
- "description": "Holdy install CLI · detects your framework and wires the SDK in 1 step",
3
+ "version": "0.3.0",
4
+ "description": "Holdy install CLI · detects your framework, wires the SDK and instruments product events",
5
5
  "license": "MIT",
6
- "homepage": "https://app.holdy.com.br",
6
+ "homepage": "https://holdy.chat",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/augustoperolligaedke/holdymvp.git",
9
+ "url": "git+https://github.com/augustoperolligaedke/holdy.git",
10
10
  "directory": "packages/install-cli"
11
11
  },
12
12
  "bin": {