@holdyapp/install 0.4.0 → 0.6.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 +10 -3
  2. package/dist/cli.js +442 -109
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,13 +5,16 @@ CLI de instalação do Holdy SDK. Detecta seu framework e wira o SDK em 1 passo.
5
5
  ## Uso
6
6
 
7
7
  ```bash
8
- npx @holdyapp/install <SUA_API_KEY>
8
+ npx @holdyapp/install
9
9
  ```
10
10
 
11
- Sem argumentos? Roda em modo interativo:
11
+ Abre o navegador pra você entrar na Holdy (ou criar a conta), conferir o código e autorizar. A chave chega sozinha no terminal, sem copiar nada. O instalador coloca o SDK no lugar certo, liga o MCP da Holdy no Claude Code e no Cursor se você quiser, e no fim espera o primeiro evento do seu app chegar. Com `--instrument`, a Holdy também lê seu código e propõe os eventos do produto.
12
+
13
+ Prefere colar a chave?
12
14
 
13
15
  ```bash
14
- npx @holdyapp/install
16
+ npx @holdyapp/install <SUA_API_KEY>
17
+ npx @holdyapp/install --paste-key
15
18
  ```
16
19
 
17
20
  ## Opções
@@ -19,6 +22,10 @@ npx @holdyapp/install
19
22
  ```
20
23
  npx @holdyapp/install <apiKey> [opções]
21
24
 
25
+ --instrument Lê o código e propõe os eventos do produto
26
+ --mcp / --no-mcp Liga (ou não) a Holdy no Claude Code e no Cursor sem perguntar
27
+ --wait / --no-wait Espera (ou não) o primeiro evento no fim
28
+ --paste-key Pede a chave no terminal em vez de abrir o navegador
22
29
  --account <id> Account ID padrão pro Holdy.init
23
30
  --no-auto-capture Desabilita captura automática de page views
24
31
  --version, -v Mostra versão
package/dist/cli.js CHANGED
@@ -2,9 +2,8 @@
2
2
  "use strict";
3
3
 
4
4
  // src/cli.ts
5
- var import_node_fs5 = require("node:fs");
6
- var import_node_readline2 = require("node:readline");
7
- var import_node_path5 = require("node:path");
5
+ var import_node_fs6 = require("node:fs");
6
+ var import_node_path6 = require("node:path");
8
7
 
9
8
  // src/detect.ts
10
9
  var import_node_fs = require("node:fs");
@@ -158,7 +157,7 @@ function injectNextApp(file, opts) {
158
157
  };
159
158
  }
160
159
  (0, import_node_fs2.writeFileSync)(file, patched);
161
- return { ok: true, modified: true, file, message: `Patched ${displayPath(file)} + criado ${displayPath(scriptPath)}` };
160
+ return { ok: true, modified: true, file, message: `Ligado em ${displayPath(file)} + criado ${displayPath(scriptPath)}` };
162
161
  }
163
162
  function buildHoldyScriptComponent(opts) {
164
163
  return `'use client'
@@ -174,8 +173,11 @@ declare global {
174
173
  interface Window {
175
174
  Holdy?: {
176
175
  init: (opts: Record<string, unknown>) => void
177
- identify: (opts: Record<string, unknown>) => void
178
- track: (primitive: string, payload?: Record<string, unknown>) => void
176
+ identify: (userId: string, traits?: Record<string, unknown>) => void
177
+ group: (accountKey: string, traits?: Record<string, unknown>) => void
178
+ track: (event: string, properties?: Record<string, unknown>) => void
179
+ page: (name?: string, properties?: Record<string, unknown>) => void
180
+ reset: () => void
179
181
  }
180
182
  }
181
183
  }
@@ -235,7 +237,7 @@ function injectNextPages(file, opts) {
235
237
  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
238
  );
237
239
  (0, import_node_fs2.writeFileSync)(file, patched);
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` };
240
+ return { ok: true, modified: true, file, message: `Ligado em ${displayPath(file)}` };
239
241
  }
240
242
  function injectViteReact(file, opts) {
241
243
  if (!file || !(0, import_node_fs2.existsSync)(file)) {
@@ -256,7 +258,7 @@ initHoldy({ apiKey: '${opts.apiKey}', ${opts.accountId ? `accountKey: '${opts.ac
256
258
 
257
259
  ${original}`;
258
260
  (0, import_node_fs2.writeFileSync)(file, patched);
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` };
261
+ return { ok: true, modified: true, file, message: `Ligado em ${displayPath(file)}` };
260
262
  }
261
263
  function injectViteVue(file, opts) {
262
264
  return injectViteReact(file, opts);
@@ -278,7 +280,7 @@ export default defineNuxtPlugin(() => {
278
280
  })
279
281
  `;
280
282
  (0, import_node_fs2.writeFileSync)(file, content);
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` };
283
+ return { ok: true, modified: true, file, message: `Criado ${displayPath(file)}` };
282
284
  }
283
285
  function injectRemix(file, opts) {
284
286
  if (!file || !(0, import_node_fs2.existsSync)(file)) {
@@ -325,7 +327,7 @@ function injectHtml(file, opts) {
325
327
  }
326
328
  const patched = original.slice(0, headEnd) + snippet + original.slice(headEnd);
327
329
  (0, import_node_fs2.writeFileSync)(file, patched);
328
- return { ok: true, modified: true, file, message: `Patched ${displayPath(file)}` };
330
+ return { ok: true, modified: true, file, message: `Ligado em ${displayPath(file)}` };
329
331
  }
330
332
  function injectHtmlOrAstro(file, opts) {
331
333
  if (!file) {
@@ -734,8 +736,273 @@ function confirm(question) {
734
736
  });
735
737
  }
736
738
 
739
+ // src/login.ts
740
+ var import_node_child_process = require("node:child_process");
741
+
742
+ // src/ui.ts
743
+ var import_node_readline2 = require("node:readline");
744
+ var isTTY = Boolean(process.stdout.isTTY) && !process.env.CI;
745
+ var color = isTTY && !process.env.NO_COLOR;
746
+ var wrap = (open, close = "\x1B[0m") => (s) => color ? `${open}${s}${close}` : s;
747
+ var bold = wrap("\x1B[1m");
748
+ var dim = wrap("\x1B[2m");
749
+ var cyan = wrap("\x1B[36m");
750
+ var green = wrap("\x1B[32m");
751
+ var yellow = wrap("\x1B[33m");
752
+ var red = wrap("\x1B[31m");
753
+ var coral = wrap("\x1B[38;2;232;107;76m");
754
+ var visible = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
755
+ function box(lines) {
756
+ const width = Math.max(...lines.map((l) => visible(l).length)) + 4;
757
+ console.log();
758
+ console.log(dim(` \u256D${"\u2500".repeat(width)}\u256E`));
759
+ for (const line of ["", ...lines, ""]) {
760
+ console.log(` ${dim("\u2502")} ${line}${" ".repeat(width - 2 - visible(line).length)}${dim("\u2502")}`);
761
+ }
762
+ console.log(dim(` \u2570${"\u2500".repeat(width)}\u256F`));
763
+ console.log();
764
+ }
765
+ var LABEL_WIDTH = 9;
766
+ function done(label, msg) {
767
+ console.log(` ${green("\u25C7")} ${bold(label.padEnd(LABEL_WIDTH))} ${msg}`);
768
+ }
769
+ function active(label, msg) {
770
+ console.log(` ${coral("\u25C6")} ${bold(label.padEnd(LABEL_WIDTH))} ${msg}`);
771
+ }
772
+ function note(msg) {
773
+ console.log(` ${dim("\u2502")} ${" ".repeat(LABEL_WIDTH)} ${msg}`);
774
+ }
775
+ function fail(label, msg) {
776
+ console.log(` ${red("\u25A0")} ${bold(label.padEnd(LABEL_WIDTH))} ${msg}`);
777
+ }
778
+ function rule() {
779
+ console.log();
780
+ console.log(dim(` ${"\u2500".repeat(52)}`));
781
+ console.log();
782
+ }
783
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
784
+ function spinner(text) {
785
+ if (!isTTY) {
786
+ console.log(` ${dim("\u2026")} ${text}`);
787
+ return { update: () => void 0, stop: () => void 0 };
788
+ }
789
+ let i = 0;
790
+ let current = text;
791
+ const draw = () => process.stdout.write(`\r\x1B[2K ${coral(FRAMES[i++ % FRAMES.length])} ${current}`);
792
+ draw();
793
+ const timer = setInterval(draw, 80);
794
+ return {
795
+ update(next) {
796
+ current = next;
797
+ },
798
+ stop() {
799
+ clearInterval(timer);
800
+ process.stdout.write("\r\x1B[2K");
801
+ }
802
+ };
803
+ }
804
+ function confirm2(question, fallback) {
805
+ if (!isTTY || !process.stdin.isTTY) return Promise.resolve(fallback);
806
+ return new Promise((resolve) => {
807
+ const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
808
+ rl.question(` ${coral("\u25C6")} ${question} ${dim(fallback ? "(S/n)" : "(s/N)")} `, (answer) => {
809
+ rl.close();
810
+ const a = answer.trim().toLowerCase();
811
+ resolve(a === "" ? fallback : a.startsWith("s") || a.startsWith("y"));
812
+ });
813
+ });
814
+ }
815
+ function ask(question) {
816
+ return new Promise((resolve) => {
817
+ const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
818
+ rl.question(` ${coral("\u25C6")} ${question} `, (answer) => {
819
+ rl.close();
820
+ resolve(answer.trim());
821
+ });
822
+ });
823
+ }
824
+ var FRAMEWORK_LABEL = {
825
+ "next-app": "Next.js (App Router)",
826
+ "next-pages": "Next.js (Pages Router)",
827
+ "vite-react": "Vite + React",
828
+ "vite-vue": "Vite + Vue",
829
+ "vite-svelte": "Vite + Svelte",
830
+ nuxt: "Nuxt",
831
+ remix: "Remix",
832
+ astro: "Astro",
833
+ "react-cra": "Create React App",
834
+ angular: "Angular",
835
+ vanilla: "HTML",
836
+ unknown: "n\xE3o identificado"
837
+ };
838
+
839
+ // src/login.ts
840
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
841
+ function openBrowser(url) {
842
+ if (process.env.CI || process.env.HOLDY_NO_BROWSER) return false;
843
+ try {
844
+ const child = process.platform === "darwin" ? (0, import_node_child_process.spawn)("open", [url], { stdio: "ignore", detached: true }) : process.platform === "win32" ? (0, import_node_child_process.spawn)("cmd", ["/c", "start", '""', url], { stdio: "ignore", detached: true }) : (0, import_node_child_process.spawn)("xdg-open", [url], { stdio: "ignore", detached: true });
845
+ child.on("error", () => void 0);
846
+ child.unref();
847
+ return true;
848
+ } catch {
849
+ return false;
850
+ }
851
+ }
852
+ async function loginWithBrowser(opts) {
853
+ let session;
854
+ try {
855
+ const res = await fetch(`${opts.apiUrl}/cli/sessions`, {
856
+ method: "POST",
857
+ headers: { "Content-Type": "application/json" },
858
+ body: JSON.stringify({ instrument: opts.instrument, read: opts.read, client: opts.client })
859
+ });
860
+ if (!res.ok) {
861
+ fail("Conta", `A Holdy n\xE3o abriu a sess\xE3o de login (HTTP ${res.status}).`);
862
+ return null;
863
+ }
864
+ session = await res.json();
865
+ } catch (err) {
866
+ fail("Conta", `N\xE3o consegui falar com a Holdy: ${err instanceof Error ? err.message : String(err)}`);
867
+ return null;
868
+ }
869
+ const opened = openBrowser(session.verification_url);
870
+ active("Conta", `${opened ? "Abri o navegador pra voc\xEA entrar na Holdy." : "Abra no navegador pra entrar na Holdy:"}`);
871
+ note(cyan(session.verification_url));
872
+ note(`C\xF3digo ${bold(session.user_code)} ${dim("\xB7 confira se \xE9 o mesmo na p\xE1gina e clique em Autorizar")}`);
873
+ const spin = spinner("Esperando voc\xEA autorizar no navegador\u2026");
874
+ const deadline = Date.now() + session.expires_in * 1e3;
875
+ let interval = Math.max(1, session.interval) * 1e3;
876
+ try {
877
+ while (Date.now() < deadline) {
878
+ await sleep(interval);
879
+ let res;
880
+ try {
881
+ res = await fetch(`${opts.apiUrl}/cli/sessions/poll`, {
882
+ method: "POST",
883
+ headers: { "Content-Type": "application/json" },
884
+ body: JSON.stringify({ device_code: session.device_code })
885
+ });
886
+ } catch {
887
+ continue;
888
+ }
889
+ if (res.status === 202) continue;
890
+ if (res.status === 429) {
891
+ interval = Math.min(interval * 2, 1e4);
892
+ continue;
893
+ }
894
+ if (res.status === 403) {
895
+ spin.stop();
896
+ fail("Conta", "Autoriza\xE7\xE3o cancelada no navegador.");
897
+ return null;
898
+ }
899
+ if (res.status === 410) break;
900
+ if (res.ok) {
901
+ const body = await res.json();
902
+ if (!body.public_key) break;
903
+ spin.stop();
904
+ return {
905
+ publicKey: body.public_key,
906
+ secretKey: body.secret_key ?? null,
907
+ organizationName: body.organization_name ?? null
908
+ };
909
+ }
910
+ }
911
+ } finally {
912
+ spin.stop();
913
+ }
914
+ fail("Conta", "O c\xF3digo expirou antes da autoriza\xE7\xE3o. Rode o comando de novo.");
915
+ return null;
916
+ }
917
+
918
+ // src/editors.ts
919
+ var import_node_child_process2 = require("node:child_process");
920
+ var import_node_fs5 = require("node:fs");
921
+ var import_node_os = require("node:os");
922
+ var import_node_path5 = require("node:path");
923
+ var EDITOR_LABEL = { "claude-code": "Claude Code", cursor: "Cursor" };
924
+ function mcpUrl(apiUrl) {
925
+ return apiUrl.replace(/\/api\/v1\/?$/, "") + "/api/mcp";
926
+ }
927
+ function hasClaudeCli() {
928
+ try {
929
+ return (0, import_node_child_process2.spawnSync)("claude", ["--version"], { stdio: "ignore", timeout: 5e3 }).status === 0;
930
+ } catch {
931
+ return false;
932
+ }
933
+ }
934
+ function detectEditors() {
935
+ const found = [];
936
+ if (hasClaudeCli()) found.push("claude-code");
937
+ if ((0, import_node_fs5.existsSync)((0, import_node_path5.join)((0, import_node_os.homedir)(), ".cursor"))) found.push("cursor");
938
+ return found;
939
+ }
940
+ function configureEditor(editor, url, key) {
941
+ if (editor === "claude-code") {
942
+ (0, import_node_child_process2.spawnSync)("claude", ["mcp", "remove", "holdy", "-s", "user"], { stdio: "ignore", timeout: 1e4 });
943
+ const res = (0, import_node_child_process2.spawnSync)(
944
+ "claude",
945
+ ["mcp", "add", "--transport", "http", "-s", "user", "holdy", url, "--header", `Authorization: Bearer ${key}`],
946
+ { encoding: "utf8", timeout: 15e3 }
947
+ );
948
+ return res.status === 0 ? { ok: true, detail: "claude mcp add holdy (escopo do usu\xE1rio)" } : { ok: false, detail: (res.stderr || res.stdout || "claude mcp add falhou").trim().split("\n")[0] };
949
+ }
950
+ const dir = (0, import_node_path5.join)((0, import_node_os.homedir)(), ".cursor");
951
+ const file = (0, import_node_path5.join)(dir, "mcp.json");
952
+ try {
953
+ let config = {};
954
+ if ((0, import_node_fs5.existsSync)(file)) config = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8") || "{}");
955
+ config.mcpServers = { ...config.mcpServers ?? {}, holdy: { url, headers: { Authorization: `Bearer ${key}` } } };
956
+ (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
957
+ (0, import_node_fs5.writeFileSync)(file, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
958
+ return { ok: true, detail: "~/.cursor/mcp.json" };
959
+ } catch (err) {
960
+ return { ok: false, detail: err instanceof Error ? err.message : "n\xE3o consegui escrever ~/.cursor/mcp.json" };
961
+ }
962
+ }
963
+
964
+ // src/first-event.ts
965
+ function keyPrefixOf(publicKey) {
966
+ const m = publicKey.match(/^hol_(?:live|test)_([A-HJKMNP-TV-Z2-9]{4})/);
967
+ return m ? m[1] : null;
968
+ }
969
+ async function readStatus(apiUrl, key, since, installKeyPrefix = null) {
970
+ try {
971
+ const params = new URLSearchParams();
972
+ if (since) params.set("since", since);
973
+ if (installKeyPrefix) params.set("key_prefix", installKeyPrefix);
974
+ const qs = params.toString() ? `?${params.toString()}` : "";
975
+ const res = await fetch(`${apiUrl}/setup/status${qs}`, { headers: { Authorization: `Bearer ${key}` } });
976
+ if (!res.ok) return null;
977
+ return await res.json();
978
+ } catch {
979
+ return null;
980
+ }
981
+ }
982
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
983
+ async function waitForFirstEvent(opts) {
984
+ const started = Date.now();
985
+ let stop = false;
986
+ const onSigint = () => {
987
+ stop = true;
988
+ };
989
+ process.once("SIGINT", onSigint);
990
+ try {
991
+ while (!stop && Date.now() - started < opts.timeoutMs) {
992
+ const status = await readStatus(opts.apiUrl, opts.key, opts.since, opts.installKeyPrefix);
993
+ if (status?.firstSince) return { status, elapsedS: Math.round((Date.now() - started) / 1e3) };
994
+ opts.onTick?.(Math.round((Date.now() - started) / 1e3));
995
+ await sleep2(2500);
996
+ }
997
+ return null;
998
+ } finally {
999
+ process.removeListener("SIGINT", onSigint);
1000
+ }
1001
+ }
1002
+
737
1003
  // src/cli.ts
738
- var VERSION = "0.4.0";
1004
+ var import_node_child_process3 = require("node:child_process");
1005
+ var VERSION = "0.6.0";
739
1006
  var DEFAULT_API_URL = "https://holdy.chat/api/v1";
740
1007
  async function main() {
741
1008
  const argv = process.argv.slice(2);
@@ -749,35 +1016,61 @@ async function main() {
749
1016
  process.exit(0);
750
1017
  }
751
1018
  const args = parseArgs(argv);
752
- log.banner();
753
- log.step("1", "Detectando seu projeto\u2026");
1019
+ const project = projectName(cwd);
1020
+ box([
1021
+ `${coral("\u25C6")} ${bold("holdy")} ${dim(`\xB7 instala\xE7\xE3o \xB7 v${VERSION}`)}`,
1022
+ dim("Do primeiro evento \xE0 primeira a\xE7\xE3o no seu produto.")
1023
+ ]);
754
1024
  const detection = detectFramework(cwd);
755
- log.detection(detection);
1025
+ done("Projeto", `${FRAMEWORK_LABEL[detection.framework] ?? detection.framework} ${dim(`\xB7 ${project}`)}`);
756
1026
  if (detection.framework === "unknown") {
757
- log.warn('N\xE3o consegui identificar o framework. Sigo no modo "vanilla" (script tag).');
1027
+ note(yellow("N\xE3o identifiquei o framework. Sigo no modo script tag."));
758
1028
  }
759
1029
  let apiKey = args.apiKey;
1030
+ let readKey = null;
1031
+ let organizationName = null;
1032
+ let editors = [];
760
1033
  if (!apiKey) {
761
- apiKey = await promptApiKey();
1034
+ if (args.mcp !== false) {
1035
+ const found = detectEditors();
1036
+ if (found.length > 0) {
1037
+ const names = found.map((e) => EDITOR_LABEL[e]).join(" e ");
1038
+ const yes = args.mcp === true || await confirm2(`Ligar a Holdy no ${names}? Seu agente passa a ler as oportunidades.`, true);
1039
+ if (yes) editors = found;
1040
+ }
1041
+ }
1042
+ if (args.pasteKey) {
1043
+ apiKey = await ask(`API key ${dim("(hol_live_\u2026 ou hol_test_\u2026)")}:`);
1044
+ } else {
1045
+ const login = await loginWithBrowser({
1046
+ apiUrl: args.apiUrl,
1047
+ instrument: args.instrument,
1048
+ read: true,
1049
+ client: { framework: detection.framework, project, cli_version: VERSION, mcp: editors.length > 0 }
1050
+ });
1051
+ if (!login) {
1052
+ note(`Prefere colar a chave? ${bold("npx @holdyapp/install --paste-key")}`);
1053
+ process.exit(1);
1054
+ }
1055
+ apiKey = login.publicKey;
1056
+ readKey = login.secretKey;
1057
+ organizationName = login.organizationName;
1058
+ done("Conta", organizationName ? `Conectado em ${bold(organizationName)}` : "Conectado");
1059
+ }
762
1060
  }
763
1061
  if (!apiKey || !apiKey.startsWith("hol_")) {
764
- log.error("API key inv\xE1lida. Use formato hol_live_\u2026 ou hol_test_\u2026 \xB7 gere em holdy.chat/settings.");
1062
+ fail("Conta", "Chave inv\xE1lida. Use hol_live_\u2026 ou hol_test_\u2026, gerada em holdy.chat/settings.");
765
1063
  process.exit(1);
766
1064
  }
767
- log.step("2", "Aplicando snippet no(s) arquivo(s)\u2026");
768
- const result = inject(detection, {
769
- apiKey,
770
- accountId: args.accountId,
771
- autoCapture: args.autoCapture
772
- });
773
- if (result.ok) {
774
- if (result.modified) {
775
- log.success(result.message);
776
- } else {
777
- log.info(result.message);
778
- }
779
- } else {
780
- log.error(result.message);
1065
+ const before = readKey ? await readStatus(args.apiUrl, readKey, null) : null;
1066
+ if (before?.topOpportunity) {
1067
+ note(
1068
+ `${coral("\u2605")} A Holdy j\xE1 tem ${before.openOpportunities} ${before.openOpportunities === 1 ? "oportunidade aberta" : "oportunidades abertas"}. A principal: ${bold(before.topOpportunity.title)}`
1069
+ );
1070
+ }
1071
+ const result = inject(detection, { apiKey, accountId: args.accountId, autoCapture: args.autoCapture });
1072
+ if (!result.ok) {
1073
+ fail("SDK", result.message);
781
1074
  if (result.manualSnippet) {
782
1075
  console.log();
783
1076
  console.log(result.manualSnippet);
@@ -785,36 +1078,113 @@ async function main() {
785
1078
  }
786
1079
  process.exit(2);
787
1080
  }
1081
+ done("SDK", result.modified ? result.message : `${result.message} ${dim("(j\xE1 estava instalado)")}`);
788
1082
  if (detection.usePackage && detection.framework !== "next-app") {
789
- log.step("3", "Falta instalar o pacote npm:");
790
- console.log();
791
- console.log(" npm install https://holdy.chat/sdk/holdyapp-sdk-1.5.0.tgz");
792
- console.log();
793
- } else {
794
- log.step("3", "Sem npm install necess\xE1rio \xB7 script tag via CDN.");
1083
+ const pm = packageManager(cwd);
1084
+ const cmd = pm === "npm" ? ["install", "@holdyapp/sdk"] : ["add", "@holdyapp/sdk"];
1085
+ const spin = spinner(`${pm} ${cmd.join(" ")}`);
1086
+ const res = (0, import_node_child_process3.spawnSync)(pm, cmd, { cwd, stdio: "ignore", shell: process.platform === "win32" });
1087
+ spin.stop();
1088
+ if (res.status === 0) done("Pacote", `@holdyapp/sdk instalado com ${pm}`);
1089
+ else fail("Pacote", `Rode voc\xEA: ${bold(`${pm} ${cmd.join(" ")}`)}`);
1090
+ }
1091
+ const mcpReady = [];
1092
+ if (editors.length > 0 && readKey) {
1093
+ const url = mcpUrl(args.apiUrl);
1094
+ for (const editor of editors) {
1095
+ const res = configureEditor(editor, url, readKey);
1096
+ if (res.ok) mcpReady.push(editor);
1097
+ else fail("Editor", `${EDITOR_LABEL[editor]}: ${res.detail}`);
1098
+ }
1099
+ if (mcpReady.length > 0) {
1100
+ done("Editor", `MCP da Holdy ligado no ${mcpReady.map((e) => EDITOR_LABEL[e]).join(" e ")}`);
1101
+ }
795
1102
  }
796
1103
  if (args.instrument) {
797
1104
  console.log();
798
- log.step("4", "Instrumentando eventos de produto\u2026");
799
1105
  const code = await runInstrument({
800
1106
  cwd,
801
- apiKey,
1107
+ apiKey: readKey ?? apiKey,
802
1108
  baseUrl: args.apiUrl,
803
1109
  framework: detection.framework,
804
1110
  sdkStyle: detection.usePackage ? "npm" : "script",
805
1111
  maxEvents: args.maxEvents,
806
1112
  dryRun: args.dryRun,
807
1113
  yes: args.yes,
808
- ui: { ...log, bold, dim, green, yellow, cyan }
1114
+ ui: {
1115
+ step: (n, msg) => console.log(` ${cyan(`[${n}]`)} ${msg}`),
1116
+ success: (msg) => note(`${green("\u2713")} ${msg}`),
1117
+ info: (msg) => note(dim(msg)),
1118
+ warn: (msg) => note(yellow(msg)),
1119
+ error: (msg) => note(red(msg)),
1120
+ bold,
1121
+ dim,
1122
+ green,
1123
+ yellow,
1124
+ cyan
1125
+ }
809
1126
  });
810
1127
  if (code !== 0) process.exit(code);
1128
+ }
1129
+ rule();
1130
+ const shouldWait = readKey !== null && args.wait !== false && (isTTY || args.wait === true);
1131
+ if (shouldWait && readKey) {
1132
+ const since = new Date(Date.now() - 5e3).toISOString();
1133
+ console.log(` ${bold("Agora rode o seu app")} ${dim(`(${devCommand(cwd)})`)} ${bold("e abra qualquer tela.")}`);
1134
+ console.log(` ${dim("Fico de olho e te mostro o primeiro evento. Ctrl+C pula.")}`);
811
1135
  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);
1136
+ const spin = spinner("Esperando o primeiro evento\u2026");
1137
+ const got = await waitForFirstEvent({
1138
+ apiUrl: args.apiUrl,
1139
+ key: readKey,
1140
+ since,
1141
+ installKeyPrefix: keyPrefixOf(apiKey),
1142
+ timeoutMs: 10 * 60 * 1e3,
1143
+ onTick: (s) => spin.update(`Esperando o primeiro evento\u2026 ${dim(`${s}s`)}`)
1144
+ });
1145
+ spin.stop();
1146
+ if (got) {
1147
+ const first = got.status.firstSince;
1148
+ const what = first.name ? `: ${cyan(first.name)}${first.path ? ` em ${bold(first.path)}` : ""}` : "";
1149
+ console.log(` ${green("\u2713")} ${bold("Primeiro evento do seu app chegou")} ${dim(`em ${got.elapsedS}s`)}${what}`);
1150
+ console.log(` ${dim("A Holdy j\xE1 est\xE1 lendo o comportamento do seu produto.")}`);
1151
+ } else {
1152
+ console.log(` ${dim("Sem evento por enquanto. Quando o app rodar com o SDK, ele aparece em holdy.chat/overview.")}`);
1153
+ }
1154
+ rule();
1155
+ }
1156
+ console.log(` ${bold("O que vem agora")}`);
1157
+ console.log();
1158
+ const steps = [
1159
+ `Ligue ${cyan("identify(userId)")} e ${cyan("group(contaId, { name, plan, mrr, status })")} depois do login. Sem group() a Holdy n\xE3o sabe de qual empresa \xE9 cada evento; com mrr e status ela mede quanto MRR cada mudan\xE7a converteu.`,
1160
+ `Abra ${cyan("https://holdy.chat/overview")}: a Holdy sugere comportamentos e abre oportunidades conforme as contas usam o produto.`
1161
+ ];
1162
+ if (mcpReady.length > 0) {
1163
+ steps.push(`No ${mcpReady.map((e) => EDITOR_LABEL[e]).join(" ou ")}, pergunte: ${bold('"quais oportunidades a Holdy encontrou?"')} ${dim("(reinicie o editor se ele j\xE1 estava aberto)")}`);
1164
+ }
1165
+ if (!args.instrument) {
1166
+ steps.push(`Quer que a Holdy proponha os eventos do seu produto? ${bold("npx @holdyapp/install --instrument")}`);
814
1167
  }
815
- log.step("4", "Pronto! Inicie o app e abra holdy.chat/onboarding \xB7 o primeiro evento aparece em segundos.");
1168
+ steps.forEach((step, i) => console.log(` ${coral(`${i + 1}.`)} ${step}`));
1169
+ console.log();
816
1170
  process.exit(0);
817
1171
  }
1172
+ function packageManager(cwd) {
1173
+ if ((0, import_node_fs6.existsSync)((0, import_node_path6.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
1174
+ if ((0, import_node_fs6.existsSync)((0, import_node_path6.join)(cwd, "yarn.lock"))) return "yarn";
1175
+ if ((0, import_node_fs6.existsSync)((0, import_node_path6.join)(cwd, "bun.lockb")) || (0, import_node_fs6.existsSync)((0, import_node_path6.join)(cwd, "bun.lock"))) return "bun";
1176
+ return "npm";
1177
+ }
1178
+ function devCommand(cwd) {
1179
+ try {
1180
+ const pkg = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path6.join)(cwd, "package.json"), "utf8"));
1181
+ const pm = packageManager(cwd);
1182
+ if (pkg.scripts?.dev) return `${pm} run dev`;
1183
+ if (pkg.scripts?.start) return `${pm} start`;
1184
+ } catch {
1185
+ }
1186
+ return "npm run dev";
1187
+ }
818
1188
  function parseArgs(argv) {
819
1189
  const out = {
820
1190
  apiKey: null,
@@ -824,7 +1194,10 @@ function parseArgs(argv) {
824
1194
  dryRun: false,
825
1195
  yes: false,
826
1196
  maxEvents: 12,
827
- apiUrl: process.env.HOLDY_API_URL ?? DEFAULT_API_URL
1197
+ apiUrl: process.env.HOLDY_API_URL ?? DEFAULT_API_URL,
1198
+ pasteKey: false,
1199
+ mcp: null,
1200
+ wait: null
828
1201
  };
829
1202
  for (let i = 0; i < argv.length; i++) {
830
1203
  const a = argv[i];
@@ -844,6 +1217,16 @@ function parseArgs(argv) {
844
1217
  } else if (a === "--max-events") {
845
1218
  const parsed = Number(argv[++i]);
846
1219
  if (Number.isFinite(parsed)) out.maxEvents = Math.min(20, Math.max(3, Math.trunc(parsed)));
1220
+ } else if (a === "--mcp") {
1221
+ out.mcp = true;
1222
+ } else if (a === "--no-mcp") {
1223
+ out.mcp = false;
1224
+ } else if (a === "--wait") {
1225
+ out.wait = true;
1226
+ } else if (a === "--no-wait") {
1227
+ out.wait = false;
1228
+ } else if (a === "--paste-key") {
1229
+ out.pasteKey = true;
847
1230
  } else if (a === "--api-url") {
848
1231
  out.apiUrl = argv[++i] ?? out.apiUrl;
849
1232
  } else if (!a.startsWith("--") && !out.apiKey) {
@@ -855,14 +1238,13 @@ function parseArgs(argv) {
855
1238
  }
856
1239
  return out;
857
1240
  }
858
- function promptApiKey() {
859
- return new Promise((resolve) => {
860
- const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
861
- rl.question(`${dim("API key")} (hol_live_\u2026 ou hol_test_\u2026): `, (answer) => {
862
- rl.close();
863
- resolve(answer.trim());
864
- });
865
- });
1241
+ function projectName(cwd) {
1242
+ try {
1243
+ const pkg = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path6.join)(cwd, "package.json"), "utf8"));
1244
+ if (pkg.name) return pkg.name;
1245
+ } catch {
1246
+ }
1247
+ return (0, import_node_path6.basename)(cwd);
866
1248
  }
867
1249
  function printHelp() {
868
1250
  const isLive = readPackageName();
@@ -870,8 +1252,9 @@ function printHelp() {
870
1252
  ${bold("@holdyapp/install")} v${VERSION} \xB7 ${isLive ?? "Holdy SDK installer"}
871
1253
 
872
1254
  ${bold("Uso:")}
1255
+ npx @holdyapp/install # entra pelo navegador, sem colar chave
1256
+ npx @holdyapp/install --instrument # idem, e j\xE1 prop\xF5e os eventos do seu produto
873
1257
  npx @holdyapp/install <apiKey> [op\xE7\xF5es]
874
- npx @holdyapp/install # modo interativo
875
1258
  HOLDY_API_KEY=hol_live_\u2026 npx @holdyapp/install # via env var
876
1259
 
877
1260
  ${bold("Argumentos:")}
@@ -884,6 +1267,9 @@ ${bold("Op\xE7\xF5es:")}
884
1267
  --dry-run S\xF3 lista os eventos propostos \xB7 n\xE3o escreve nada (implica --instrument)
885
1268
  --max-events <n> Teto de eventos propostos \xB7 3 a 20 \xB7 default 12
886
1269
  --yes, -y Aplica o diff sem perguntar
1270
+ --mcp / --no-mcp Liga (ou n\xE3o) a Holdy no Claude Code e no Cursor sem perguntar
1271
+ --wait / --no-wait Espera (ou n\xE3o) o primeiro evento no fim
1272
+ --paste-key Pede a chave no terminal em vez de abrir o navegador
887
1273
  --api-url <url> Override da API (default https://holdy.chat/api/v1)
888
1274
  --version, -v Mostra vers\xE3o
889
1275
  --help, -h Mostra esta mensagem
@@ -900,73 +1286,20 @@ ${bold("Exemplos:")}
900
1286
 
901
1287
  ${bold("Sobre --instrument:")}
902
1288
  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,
1289
+ Pelo navegador, a Holdy entrega uma chave de leitura pra isso (24 horas, ou sem validade se for pro MCP).
1290
+ Com chave colada, exige key SECRETA (n\xE3o a p\xFAblica do browser). Voc\xEA v\xEA o diff antes de aplicar,
904
1291
  e nada \xE9 escrito sem confirma\xE7\xE3o. Pageview e rota j\xE1 s\xE3o autom\xE1ticos \u2014 o
905
1292
  planner s\xF3 prop\xF5e momentos em que a conta avan\xE7a no produto.
906
1293
  `);
907
1294
  }
908
1295
  function readPackageName() {
909
1296
  try {
910
- const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(__dirname, "..", "package.json"), "utf8"));
1297
+ const pkg = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path6.join)(__dirname, "..", "package.json"), "utf8"));
911
1298
  return pkg.description ?? null;
912
1299
  } catch {
913
1300
  return null;
914
1301
  }
915
1302
  }
916
- var COLORS = {
917
- reset: "\x1B[0m",
918
- bold: "\x1B[1m",
919
- dim: "\x1B[2m",
920
- cyan: "\x1B[36m",
921
- green: "\x1B[32m",
922
- red: "\x1B[31m",
923
- yellow: "\x1B[33m"
924
- };
925
- function bold(s) {
926
- return `${COLORS.bold}${s}${COLORS.reset}`;
927
- }
928
- function dim(s) {
929
- return `${COLORS.dim}${s}${COLORS.reset}`;
930
- }
931
- function cyan(s) {
932
- return `${COLORS.cyan}${s}${COLORS.reset}`;
933
- }
934
- function green(s) {
935
- return `${COLORS.green}${s}${COLORS.reset}`;
936
- }
937
- function red(s) {
938
- return `${COLORS.red}${s}${COLORS.reset}`;
939
- }
940
- function yellow(s) {
941
- return `${COLORS.yellow}${s}${COLORS.reset}`;
942
- }
943
- var log = {
944
- banner() {
945
- console.log();
946
- console.log(` ${bold("Holdy")} ${dim("\xB7 behavioral operating layer")}`);
947
- console.log();
948
- },
949
- step(n, msg) {
950
- console.log(` ${cyan(`[${n}]`)} ${msg}`);
951
- },
952
- detection(d) {
953
- const label = d.framework === "unknown" ? "?" : d.framework;
954
- const file = d.targetFile ? dim(` \u2192 ${d.targetFile.replace(process.cwd() + "/", "")}`) : "";
955
- console.log(` ${dim("framework:")} ${bold(label)}${file}`);
956
- },
957
- success(msg) {
958
- console.log(` ${green("\u2713")} ${msg}`);
959
- },
960
- info(msg) {
961
- console.log(` ${dim("\xB7")} ${msg}`);
962
- },
963
- warn(msg) {
964
- console.log(` ${yellow("!")} ${msg}`);
965
- },
966
- error(msg) {
967
- console.log(` ${red("\u2717")} ${msg}`);
968
- }
969
- };
970
1303
  main().catch((err) => {
971
1304
  console.error(red("[holdy-install] erro inesperado:"), err instanceof Error ? err.message : err);
972
1305
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holdyapp/install",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
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",