@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.
- package/README.md +10 -3
- package/dist/cli.js +442 -109
- 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
|
|
8
|
+
npx @holdyapp/install
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
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
|
|
6
|
-
var
|
|
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: `
|
|
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: (
|
|
178
|
-
|
|
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: `
|
|
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: `
|
|
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)}
|
|
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: `
|
|
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
|
|
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
|
-
|
|
753
|
-
|
|
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
|
-
|
|
1025
|
+
done("Projeto", `${FRAMEWORK_LABEL[detection.framework] ?? detection.framework} ${dim(`\xB7 ${project}`)}`);
|
|
756
1026
|
if (detection.framework === "unknown") {
|
|
757
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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: {
|
|
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
|
-
|
|
813
|
-
|
|
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
|
-
|
|
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
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
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,
|
|
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