@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.
- package/README.md +3 -1
- package/dist/cli.js +389 -167
- 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
|
|
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
|
|
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'
|
|
@@ -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: `
|
|
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: `
|
|
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)}
|
|
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: `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
);
|
|
778
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
if (
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
834
|
-
|
|
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
|
-
|
|
1025
|
+
done("Projeto", `${FRAMEWORK_LABEL[detection.framework] ?? detection.framework} ${dim(`\xB7 ${project}`)}`);
|
|
837
1026
|
if (detection.framework === "unknown") {
|
|
838
|
-
|
|
1027
|
+
note(yellow("N\xE3o identifiquei o framework. Sigo no modo script tag."));
|
|
839
1028
|
}
|
|
840
1029
|
let apiKey = args.apiKey;
|
|
841
|
-
let
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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:
|
|
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: {
|
|
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
|
-
|
|
910
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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