@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.
- package/README.md +1 -1
- package/dist/cli.js +414 -23
- 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
|
|
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
|
|
6
|
-
var
|
|
7
|
-
var
|
|
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://
|
|
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 ? `
|
|
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 {
|
|
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) {
|
|
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 {
|
|
255
|
-
|
|
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 {
|
|
270
|
+
import { initHoldy } from '@holdyapp/sdk/web'
|
|
271
271
|
|
|
272
272
|
export default defineNuxtPlugin(() => {
|
|
273
|
-
|
|
273
|
+
initHoldy({
|
|
274
274
|
apiKey: '${opts.apiKey}',
|
|
275
|
-
${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
|
-
|
|
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
|
-
|
|
356
|
-
return `import {
|
|
355
|
+
accountKey: '${opts.accountId}',` : "";
|
|
356
|
+
return `import { initHoldy } from '@holdyapp/sdk/web'
|
|
357
357
|
|
|
358
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 = {
|
|
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,
|
|
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>
|
|
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,
|
|
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.
|
|
4
|
-
"description": "Holdy install CLI · detects your framework
|
|
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://
|
|
6
|
+
"homepage": "https://holdy.chat",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/augustoperolligaedke/
|
|
9
|
+
"url": "git+https://github.com/augustoperolligaedke/holdy.git",
|
|
10
10
|
"directory": "packages/install-cli"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|