@holdyapp/install 0.5.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 +3 -1
  2. package/dist/cli.js +389 -167
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@ CLI de instalação do Holdy SDK. Detecta seu framework e wira o SDK em 1 passo.
8
8
  npx @holdyapp/install
9
9
  ```
10
10
 
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. Com `--instrument`, a Holdy também lê seu código e propõe os eventos do produto, usando uma chave secreta que vale 1 hora.
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
12
 
13
13
  Prefere colar a chave?
14
14
 
@@ -23,6 +23,8 @@ npx @holdyapp/install --paste-key
23
23
  npx @holdyapp/install <apiKey> [opções]
24
24
 
25
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
26
28
  --paste-key Pede a chave no terminal em vez de abrir o navegador
27
29
  --account <id> Account ID padrão pro Holdy.init
28
30
  --no-auto-capture Desabilita captura automática de page views
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'
@@ -238,7 +237,7 @@ function injectNextPages(file, opts) {
238
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 }`
239
238
  );
240
239
  (0, import_node_fs2.writeFileSync)(file, patched);
241
- return { ok: true, modified: true, file, message: `Patched ${displayPath(file)} \xB7 rode npm install @holdyapp/sdk` };
240
+ return { ok: true, modified: true, file, message: `Ligado em ${displayPath(file)}` };
242
241
  }
243
242
  function injectViteReact(file, opts) {
244
243
  if (!file || !(0, import_node_fs2.existsSync)(file)) {
@@ -259,7 +258,7 @@ initHoldy({ apiKey: '${opts.apiKey}', ${opts.accountId ? `accountKey: '${opts.ac
259
258
 
260
259
  ${original}`;
261
260
  (0, import_node_fs2.writeFileSync)(file, patched);
262
- return { ok: true, modified: true, file, message: `Patched ${displayPath(file)} \xB7 rode npm install @holdyapp/sdk` };
261
+ return { ok: true, modified: true, file, message: `Ligado em ${displayPath(file)}` };
263
262
  }
264
263
  function injectViteVue(file, opts) {
265
264
  return injectViteReact(file, opts);
@@ -281,7 +280,7 @@ export default defineNuxtPlugin(() => {
281
280
  })
282
281
  `;
283
282
  (0, import_node_fs2.writeFileSync)(file, content);
284
- return { ok: true, modified: true, file, message: `Criado ${displayPath(file)} \xB7 rode npm install @holdyapp/sdk` };
283
+ return { ok: true, modified: true, file, message: `Criado ${displayPath(file)}` };
285
284
  }
286
285
  function injectRemix(file, opts) {
287
286
  if (!file || !(0, import_node_fs2.existsSync)(file)) {
@@ -328,7 +327,7 @@ function injectHtml(file, opts) {
328
327
  }
329
328
  const patched = original.slice(0, headEnd) + snippet + original.slice(headEnd);
330
329
  (0, import_node_fs2.writeFileSync)(file, patched);
331
- return { ok: true, modified: true, file, message: `Patched ${displayPath(file)}` };
330
+ return { ok: true, modified: true, file, message: `Ligado em ${displayPath(file)}` };
332
331
  }
333
332
  function injectHtmlOrAstro(file, opts) {
334
333
  if (!file) {
@@ -739,6 +738,105 @@ function confirm(question) {
739
738
 
740
739
  // src/login.ts
741
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
742
840
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
743
841
  function openBrowser(url) {
744
842
  if (process.env.CI || process.env.HOLDY_NO_BROWSER) return false;
@@ -752,71 +850,159 @@ function openBrowser(url) {
752
850
  }
753
851
  }
754
852
  async function loginWithBrowser(opts) {
755
- const { ui } = opts;
756
853
  let session;
757
854
  try {
758
855
  const res = await fetch(`${opts.apiUrl}/cli/sessions`, {
759
856
  method: "POST",
760
857
  headers: { "Content-Type": "application/json" },
761
- body: JSON.stringify({ instrument: opts.instrument, client: opts.client })
858
+ body: JSON.stringify({ instrument: opts.instrument, read: opts.read, client: opts.client })
762
859
  });
763
860
  if (!res.ok) {
764
- ui.error(`A Holdy n\xE3o abriu a sess\xE3o de login (HTTP ${res.status}).`);
861
+ fail("Conta", `A Holdy n\xE3o abriu a sess\xE3o de login (HTTP ${res.status}).`);
765
862
  return null;
766
863
  }
767
864
  session = await res.json();
768
865
  } catch (err) {
769
- ui.error(`N\xE3o consegui falar com a Holdy: ${err instanceof Error ? err.message : String(err)}`);
866
+ fail("Conta", `N\xE3o consegui falar com a Holdy: ${err instanceof Error ? err.message : String(err)}`);
770
867
  return null;
771
868
  }
772
- console.log();
773
- console.log(` Seu c\xF3digo: ${ui.bold(session.user_code)}`);
774
869
  const opened = openBrowser(session.verification_url);
775
- console.log(
776
- ` ${opened ? "Abri o navegador. Se n\xE3o abriu, acesse:" : "Abra no navegador:"} ${ui.cyan(session.verification_url)}`
777
- );
778
- console.log(` ${ui.dim("Entre ou crie sua conta, confira o c\xF3digo e clique em Autorizar.")}`);
779
- console.log();
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");
780
874
  const deadline = Date.now() + session.expires_in * 1e3;
781
875
  let interval = Math.max(1, session.interval) * 1e3;
782
- while (Date.now() < deadline) {
783
- await sleep(interval);
784
- let res;
785
- try {
786
- res = await fetch(`${opts.apiUrl}/cli/sessions/poll`, {
787
- method: "POST",
788
- headers: { "Content-Type": "application/json" },
789
- body: JSON.stringify({ device_code: session.device_code })
790
- });
791
- } catch {
792
- continue;
793
- }
794
- if (res.status === 202) continue;
795
- if (res.status === 429) {
796
- interval = Math.min(interval * 2, 1e4);
797
- continue;
798
- }
799
- if (res.status === 403) {
800
- ui.error("Autoriza\xE7\xE3o cancelada no navegador.");
801
- return null;
802
- }
803
- if (res.status === 410) break;
804
- if (res.ok) {
805
- const body = await res.json();
806
- if (!body.public_key) break;
807
- return {
808
- publicKey: body.public_key,
809
- secretKey: body.secret_key ?? null,
810
- organizationName: body.organization_name ?? null
811
- };
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
+ }
812
910
  }
911
+ } finally {
912
+ spin.stop();
813
913
  }
814
- ui.error("O c\xF3digo expirou antes da autoriza\xE7\xE3o. Rode o comando de novo.");
914
+ fail("Conta", "O c\xF3digo expirou antes da autoriza\xE7\xE3o. Rode o comando de novo.");
815
915
  return null;
816
916
  }
817
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
+
818
1003
  // src/cli.ts
819
- var VERSION = "0.5.0";
1004
+ var import_node_child_process3 = require("node:child_process");
1005
+ var VERSION = "0.6.0";
820
1006
  var DEFAULT_API_URL = "https://holdy.chat/api/v1";
821
1007
  async function main() {
822
1008
  const argv = process.argv.slice(2);
@@ -830,51 +1016,61 @@ async function main() {
830
1016
  process.exit(0);
831
1017
  }
832
1018
  const args = parseArgs(argv);
833
- log.banner();
834
- 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
+ ]);
835
1024
  const detection = detectFramework(cwd);
836
- log.detection(detection);
1025
+ done("Projeto", `${FRAMEWORK_LABEL[detection.framework] ?? detection.framework} ${dim(`\xB7 ${project}`)}`);
837
1026
  if (detection.framework === "unknown") {
838
- 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."));
839
1028
  }
840
1029
  let apiKey = args.apiKey;
841
- let instrumentKey = null;
842
- if (!apiKey && args.pasteKey) {
843
- apiKey = await promptApiKey();
844
- } else if (!apiKey) {
845
- log.step("2", "Entrando na Holdy pelo navegador\u2026");
846
- const login = await loginWithBrowser({
847
- apiUrl: args.apiUrl,
848
- instrument: args.instrument,
849
- client: { framework: detection.framework, project: projectName(cwd), cli_version: VERSION },
850
- ui: { ...log, bold, dim, cyan }
851
- });
852
- if (!login) {
853
- log.info(`Prefere colar a chave? ${bold("npx @holdyapp/install --paste-key")}`);
854
- process.exit(1);
1030
+ let readKey = null;
1031
+ let organizationName = null;
1032
+ let editors = [];
1033
+ if (!apiKey) {
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");
855
1059
  }
856
- log.success(login.organizationName ? `Autorizado em ${bold(login.organizationName)}` : "Autorizado");
857
- apiKey = login.publicKey;
858
- instrumentKey = login.secretKey;
859
1060
  }
860
1061
  if (!apiKey || !apiKey.startsWith("hol_")) {
861
- 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.");
862
1063
  process.exit(1);
863
1064
  }
864
- log.step("3", "Aplicando snippet no(s) arquivo(s)\u2026");
865
- const result = inject(detection, {
866
- apiKey,
867
- accountId: args.accountId,
868
- autoCapture: args.autoCapture
869
- });
870
- if (result.ok) {
871
- if (result.modified) {
872
- log.success(result.message);
873
- } else {
874
- log.info(result.message);
875
- }
876
- } else {
877
- 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);
878
1074
  if (result.manualSnippet) {
879
1075
  console.log();
880
1076
  console.log(result.manualSnippet);
@@ -882,36 +1078,113 @@ async function main() {
882
1078
  }
883
1079
  process.exit(2);
884
1080
  }
1081
+ done("SDK", result.modified ? result.message : `${result.message} ${dim("(j\xE1 estava instalado)")}`);
885
1082
  if (detection.usePackage && detection.framework !== "next-app") {
886
- log.step("4", "Falta instalar o pacote npm:");
887
- console.log();
888
- console.log(" npm install @holdyapp/sdk");
889
- console.log();
890
- } else {
891
- log.step("4", "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
+ }
892
1102
  }
893
1103
  if (args.instrument) {
894
1104
  console.log();
895
- log.step("5", "Instrumentando eventos de produto\u2026");
896
1105
  const code = await runInstrument({
897
1106
  cwd,
898
- apiKey: instrumentKey ?? apiKey,
1107
+ apiKey: readKey ?? apiKey,
899
1108
  baseUrl: args.apiUrl,
900
1109
  framework: detection.framework,
901
1110
  sdkStyle: detection.usePackage ? "npm" : "script",
902
1111
  maxEvents: args.maxEvents,
903
1112
  dryRun: args.dryRun,
904
1113
  yes: args.yes,
905
- 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
+ }
906
1126
  });
907
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.")}`);
908
1135
  console.log();
909
- log.step("6", "Pronto! Suba o app, use o produto e veja os eventos chegando em holdy.chat.");
910
- 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")}`);
911
1167
  }
912
- log.step("5", "Pronto! Inicie o app e abra holdy.chat/overview \xB7 o primeiro evento aparece em segundos.");
1168
+ steps.forEach((step, i) => console.log(` ${coral(`${i + 1}.`)} ${step}`));
1169
+ console.log();
913
1170
  process.exit(0);
914
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
+ }
915
1188
  function parseArgs(argv) {
916
1189
  const out = {
917
1190
  apiKey: null,
@@ -922,7 +1195,9 @@ function parseArgs(argv) {
922
1195
  yes: false,
923
1196
  maxEvents: 12,
924
1197
  apiUrl: process.env.HOLDY_API_URL ?? DEFAULT_API_URL,
925
- pasteKey: false
1198
+ pasteKey: false,
1199
+ mcp: null,
1200
+ wait: null
926
1201
  };
927
1202
  for (let i = 0; i < argv.length; i++) {
928
1203
  const a = argv[i];
@@ -942,6 +1217,14 @@ function parseArgs(argv) {
942
1217
  } else if (a === "--max-events") {
943
1218
  const parsed = Number(argv[++i]);
944
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;
945
1228
  } else if (a === "--paste-key") {
946
1229
  out.pasteKey = true;
947
1230
  } else if (a === "--api-url") {
@@ -957,20 +1240,11 @@ function parseArgs(argv) {
957
1240
  }
958
1241
  function projectName(cwd) {
959
1242
  try {
960
- const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(cwd, "package.json"), "utf8"));
1243
+ const pkg = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path6.join)(cwd, "package.json"), "utf8"));
961
1244
  if (pkg.name) return pkg.name;
962
1245
  } catch {
963
1246
  }
964
- return (0, import_node_path5.basename)(cwd);
965
- }
966
- function promptApiKey() {
967
- return new Promise((resolve) => {
968
- const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
969
- rl.question(`${dim("API key")} (hol_live_\u2026 ou hol_test_\u2026): `, (answer) => {
970
- rl.close();
971
- resolve(answer.trim());
972
- });
973
- });
1247
+ return (0, import_node_path6.basename)(cwd);
974
1248
  }
975
1249
  function printHelp() {
976
1250
  const isLive = readPackageName();
@@ -993,6 +1267,8 @@ ${bold("Op\xE7\xF5es:")}
993
1267
  --dry-run S\xF3 lista os eventos propostos \xB7 n\xE3o escreve nada (implica --instrument)
994
1268
  --max-events <n> Teto de eventos propostos \xB7 3 a 20 \xB7 default 12
995
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
996
1272
  --paste-key Pede a chave no terminal em vez de abrir o navegador
997
1273
  --api-url <url> Override da API (default https://holdy.chat/api/v1)
998
1274
  --version, -v Mostra vers\xE3o
@@ -1010,7 +1286,7 @@ ${bold("Exemplos:")}
1010
1286
 
1011
1287
  ${bold("Sobre --instrument:")}
1012
1288
  Manda trechos do seu c\xF3digo pra Holdy, que devolve onde inserir cada track().
1013
- Pelo navegador, a Holdy entrega uma chave secreta que vale 1 hora s\xF3 pra isso.
1289
+ Pelo navegador, a Holdy entrega uma chave de leitura pra isso (24 horas, ou sem validade se for pro MCP).
1014
1290
  Com chave colada, exige key SECRETA (n\xE3o a p\xFAblica do browser). Voc\xEA v\xEA o diff antes de aplicar,
1015
1291
  e nada \xE9 escrito sem confirma\xE7\xE3o. Pageview e rota j\xE1 s\xE3o autom\xE1ticos \u2014 o
1016
1292
  planner s\xF3 prop\xF5e momentos em que a conta avan\xE7a no produto.
@@ -1018,66 +1294,12 @@ ${bold("Sobre --instrument:")}
1018
1294
  }
1019
1295
  function readPackageName() {
1020
1296
  try {
1021
- 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"));
1022
1298
  return pkg.description ?? null;
1023
1299
  } catch {
1024
1300
  return null;
1025
1301
  }
1026
1302
  }
1027
- var COLORS = {
1028
- reset: "\x1B[0m",
1029
- bold: "\x1B[1m",
1030
- dim: "\x1B[2m",
1031
- cyan: "\x1B[36m",
1032
- green: "\x1B[32m",
1033
- red: "\x1B[31m",
1034
- yellow: "\x1B[33m"
1035
- };
1036
- function bold(s) {
1037
- return `${COLORS.bold}${s}${COLORS.reset}`;
1038
- }
1039
- function dim(s) {
1040
- return `${COLORS.dim}${s}${COLORS.reset}`;
1041
- }
1042
- function cyan(s) {
1043
- return `${COLORS.cyan}${s}${COLORS.reset}`;
1044
- }
1045
- function green(s) {
1046
- return `${COLORS.green}${s}${COLORS.reset}`;
1047
- }
1048
- function red(s) {
1049
- return `${COLORS.red}${s}${COLORS.reset}`;
1050
- }
1051
- function yellow(s) {
1052
- return `${COLORS.yellow}${s}${COLORS.reset}`;
1053
- }
1054
- var log = {
1055
- banner() {
1056
- console.log();
1057
- console.log(` ${bold("Holdy")} ${dim("\xB7 behavioral operating layer")}`);
1058
- console.log();
1059
- },
1060
- step(n, msg) {
1061
- console.log(` ${cyan(`[${n}]`)} ${msg}`);
1062
- },
1063
- detection(d) {
1064
- const label = d.framework === "unknown" ? "?" : d.framework;
1065
- const file = d.targetFile ? dim(` \u2192 ${d.targetFile.replace(process.cwd() + "/", "")}`) : "";
1066
- console.log(` ${dim("framework:")} ${bold(label)}${file}`);
1067
- },
1068
- success(msg) {
1069
- console.log(` ${green("\u2713")} ${msg}`);
1070
- },
1071
- info(msg) {
1072
- console.log(` ${dim("\xB7")} ${msg}`);
1073
- },
1074
- warn(msg) {
1075
- console.log(` ${yellow("!")} ${msg}`);
1076
- },
1077
- error(msg) {
1078
- console.log(` ${red("\u2717")} ${msg}`);
1079
- }
1080
- };
1081
1303
  main().catch((err) => {
1082
1304
  console.error(red("[holdy-install] erro inesperado:"), err instanceof Error ? err.message : err);
1083
1305
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holdyapp/install",
3
- "version": "0.5.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",