@holdyapp/install 0.3.0 → 0.5.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 +8 -3
- package/dist/cli.js +143 -16
- 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. 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.
|
|
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,8 @@ 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
|
+
--paste-key Pede a chave no terminal em vez de abrir o navegador
|
|
22
27
|
--account <id> Account ID padrão pro Holdy.init
|
|
23
28
|
--no-auto-capture Desabilita captura automática de page views
|
|
24
29
|
--version, -v Mostra versão
|
package/dist/cli.js
CHANGED
|
@@ -174,8 +174,11 @@ declare global {
|
|
|
174
174
|
interface Window {
|
|
175
175
|
Holdy?: {
|
|
176
176
|
init: (opts: Record<string, unknown>) => void
|
|
177
|
-
identify: (
|
|
178
|
-
|
|
177
|
+
identify: (userId: string, traits?: Record<string, unknown>) => void
|
|
178
|
+
group: (accountKey: string, traits?: Record<string, unknown>) => void
|
|
179
|
+
track: (event: string, properties?: Record<string, unknown>) => void
|
|
180
|
+
page: (name?: string, properties?: Record<string, unknown>) => void
|
|
181
|
+
reset: () => void
|
|
179
182
|
}
|
|
180
183
|
}
|
|
181
184
|
}
|
|
@@ -592,11 +595,27 @@ async function runInstrument(opts) {
|
|
|
592
595
|
`${patches.length} ${patches.length === 1 ? "evento inserido" : "eventos inseridos"} em ${written.length} ${written.length === 1 ? "arquivo" : "arquivos"}`
|
|
593
596
|
);
|
|
594
597
|
for (const file of written) ui.info(file);
|
|
598
|
+
printMetrics(plan.metrics, ui);
|
|
595
599
|
console.log();
|
|
596
600
|
ui.info("Os eventos se registram sozinhos na taxonomia no primeiro emit \u2014 n\xE3o precisa declarar nada na Holdy.");
|
|
597
|
-
ui.info("Rode o app, use o produto e acompanhe em /
|
|
601
|
+
ui.info("Rode o app, use o produto e acompanhe em holdy.chat/onboarding.");
|
|
598
602
|
return 0;
|
|
599
603
|
}
|
|
604
|
+
function printMetrics(metrics, ui) {
|
|
605
|
+
if (!metrics || metrics.length === 0) return;
|
|
606
|
+
const prontas = metrics.filter((m) => m.ready);
|
|
607
|
+
const faltando = metrics.filter((m) => !m.ready);
|
|
608
|
+
console.log();
|
|
609
|
+
console.log(` ${ui.bold("O que voc\xEA passa a medir")}`);
|
|
610
|
+
console.log();
|
|
611
|
+
for (const m of prontas) {
|
|
612
|
+
const extra = m.needs_choice ? ui.dim(` \xB7 ${m.needs_choice}`) : "";
|
|
613
|
+
console.log(` ${ui.green("\u2713")} ${m.label}${extra}`);
|
|
614
|
+
}
|
|
615
|
+
for (const m of faltando) {
|
|
616
|
+
console.log(` ${ui.dim("\xB7")} ${ui.dim(`${m.label} \u2014 falta evento de fase ${m.missing.join(" e ")}`)}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
600
619
|
async function requestPlan(opts, files) {
|
|
601
620
|
const url = `${opts.baseUrl.replace(/\/$/, "")}/instrumentation/plan`;
|
|
602
621
|
const response = await fetch(url, {
|
|
@@ -718,8 +737,86 @@ function confirm(question) {
|
|
|
718
737
|
});
|
|
719
738
|
}
|
|
720
739
|
|
|
740
|
+
// src/login.ts
|
|
741
|
+
var import_node_child_process = require("node:child_process");
|
|
742
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
743
|
+
function openBrowser(url) {
|
|
744
|
+
if (process.env.CI || process.env.HOLDY_NO_BROWSER) return false;
|
|
745
|
+
try {
|
|
746
|
+
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 });
|
|
747
|
+
child.on("error", () => void 0);
|
|
748
|
+
child.unref();
|
|
749
|
+
return true;
|
|
750
|
+
} catch {
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
async function loginWithBrowser(opts) {
|
|
755
|
+
const { ui } = opts;
|
|
756
|
+
let session;
|
|
757
|
+
try {
|
|
758
|
+
const res = await fetch(`${opts.apiUrl}/cli/sessions`, {
|
|
759
|
+
method: "POST",
|
|
760
|
+
headers: { "Content-Type": "application/json" },
|
|
761
|
+
body: JSON.stringify({ instrument: opts.instrument, client: opts.client })
|
|
762
|
+
});
|
|
763
|
+
if (!res.ok) {
|
|
764
|
+
ui.error(`A Holdy n\xE3o abriu a sess\xE3o de login (HTTP ${res.status}).`);
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
session = await res.json();
|
|
768
|
+
} catch (err) {
|
|
769
|
+
ui.error(`N\xE3o consegui falar com a Holdy: ${err instanceof Error ? err.message : String(err)}`);
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
console.log();
|
|
773
|
+
console.log(` Seu c\xF3digo: ${ui.bold(session.user_code)}`);
|
|
774
|
+
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();
|
|
780
|
+
const deadline = Date.now() + session.expires_in * 1e3;
|
|
781
|
+
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
|
+
};
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
ui.error("O c\xF3digo expirou antes da autoriza\xE7\xE3o. Rode o comando de novo.");
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
|
|
721
818
|
// src/cli.ts
|
|
722
|
-
var VERSION = "0.
|
|
819
|
+
var VERSION = "0.5.0";
|
|
723
820
|
var DEFAULT_API_URL = "https://holdy.chat/api/v1";
|
|
724
821
|
async function main() {
|
|
725
822
|
const argv = process.argv.slice(2);
|
|
@@ -741,14 +838,30 @@ async function main() {
|
|
|
741
838
|
log.warn('N\xE3o consegui identificar o framework. Sigo no modo "vanilla" (script tag).');
|
|
742
839
|
}
|
|
743
840
|
let apiKey = args.apiKey;
|
|
744
|
-
|
|
841
|
+
let instrumentKey = null;
|
|
842
|
+
if (!apiKey && args.pasteKey) {
|
|
745
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);
|
|
855
|
+
}
|
|
856
|
+
log.success(login.organizationName ? `Autorizado em ${bold(login.organizationName)}` : "Autorizado");
|
|
857
|
+
apiKey = login.publicKey;
|
|
858
|
+
instrumentKey = login.secretKey;
|
|
746
859
|
}
|
|
747
860
|
if (!apiKey || !apiKey.startsWith("hol_")) {
|
|
748
861
|
log.error("API key inv\xE1lida. Use formato hol_live_\u2026 ou hol_test_\u2026 \xB7 gere em holdy.chat/settings.");
|
|
749
862
|
process.exit(1);
|
|
750
863
|
}
|
|
751
|
-
log.step("
|
|
864
|
+
log.step("3", "Aplicando snippet no(s) arquivo(s)\u2026");
|
|
752
865
|
const result = inject(detection, {
|
|
753
866
|
apiKey,
|
|
754
867
|
accountId: args.accountId,
|
|
@@ -769,20 +882,20 @@ async function main() {
|
|
|
769
882
|
}
|
|
770
883
|
process.exit(2);
|
|
771
884
|
}
|
|
772
|
-
if (detection.usePackage) {
|
|
773
|
-
log.step("
|
|
885
|
+
if (detection.usePackage && detection.framework !== "next-app") {
|
|
886
|
+
log.step("4", "Falta instalar o pacote npm:");
|
|
774
887
|
console.log();
|
|
775
888
|
console.log(" npm install @holdyapp/sdk");
|
|
776
889
|
console.log();
|
|
777
890
|
} else {
|
|
778
|
-
log.step("
|
|
891
|
+
log.step("4", "Sem npm install necess\xE1rio \xB7 script tag via CDN.");
|
|
779
892
|
}
|
|
780
893
|
if (args.instrument) {
|
|
781
894
|
console.log();
|
|
782
|
-
log.step("
|
|
895
|
+
log.step("5", "Instrumentando eventos de produto\u2026");
|
|
783
896
|
const code = await runInstrument({
|
|
784
897
|
cwd,
|
|
785
|
-
apiKey,
|
|
898
|
+
apiKey: instrumentKey ?? apiKey,
|
|
786
899
|
baseUrl: args.apiUrl,
|
|
787
900
|
framework: detection.framework,
|
|
788
901
|
sdkStyle: detection.usePackage ? "npm" : "script",
|
|
@@ -793,10 +906,10 @@ async function main() {
|
|
|
793
906
|
});
|
|
794
907
|
if (code !== 0) process.exit(code);
|
|
795
908
|
console.log();
|
|
796
|
-
log.step("
|
|
909
|
+
log.step("6", "Pronto! Suba o app, use o produto e veja os eventos chegando em holdy.chat.");
|
|
797
910
|
process.exit(0);
|
|
798
911
|
}
|
|
799
|
-
log.step("
|
|
912
|
+
log.step("5", "Pronto! Inicie o app e abra holdy.chat/overview \xB7 o primeiro evento aparece em segundos.");
|
|
800
913
|
process.exit(0);
|
|
801
914
|
}
|
|
802
915
|
function parseArgs(argv) {
|
|
@@ -808,7 +921,8 @@ function parseArgs(argv) {
|
|
|
808
921
|
dryRun: false,
|
|
809
922
|
yes: false,
|
|
810
923
|
maxEvents: 12,
|
|
811
|
-
apiUrl: process.env.HOLDY_API_URL ?? DEFAULT_API_URL
|
|
924
|
+
apiUrl: process.env.HOLDY_API_URL ?? DEFAULT_API_URL,
|
|
925
|
+
pasteKey: false
|
|
812
926
|
};
|
|
813
927
|
for (let i = 0; i < argv.length; i++) {
|
|
814
928
|
const a = argv[i];
|
|
@@ -828,6 +942,8 @@ function parseArgs(argv) {
|
|
|
828
942
|
} else if (a === "--max-events") {
|
|
829
943
|
const parsed = Number(argv[++i]);
|
|
830
944
|
if (Number.isFinite(parsed)) out.maxEvents = Math.min(20, Math.max(3, Math.trunc(parsed)));
|
|
945
|
+
} else if (a === "--paste-key") {
|
|
946
|
+
out.pasteKey = true;
|
|
831
947
|
} else if (a === "--api-url") {
|
|
832
948
|
out.apiUrl = argv[++i] ?? out.apiUrl;
|
|
833
949
|
} else if (!a.startsWith("--") && !out.apiKey) {
|
|
@@ -839,6 +955,14 @@ function parseArgs(argv) {
|
|
|
839
955
|
}
|
|
840
956
|
return out;
|
|
841
957
|
}
|
|
958
|
+
function projectName(cwd) {
|
|
959
|
+
try {
|
|
960
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(cwd, "package.json"), "utf8"));
|
|
961
|
+
if (pkg.name) return pkg.name;
|
|
962
|
+
} catch {
|
|
963
|
+
}
|
|
964
|
+
return (0, import_node_path5.basename)(cwd);
|
|
965
|
+
}
|
|
842
966
|
function promptApiKey() {
|
|
843
967
|
return new Promise((resolve) => {
|
|
844
968
|
const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
@@ -854,8 +978,9 @@ function printHelp() {
|
|
|
854
978
|
${bold("@holdyapp/install")} v${VERSION} \xB7 ${isLive ?? "Holdy SDK installer"}
|
|
855
979
|
|
|
856
980
|
${bold("Uso:")}
|
|
981
|
+
npx @holdyapp/install # entra pelo navegador, sem colar chave
|
|
982
|
+
npx @holdyapp/install --instrument # idem, e j\xE1 prop\xF5e os eventos do seu produto
|
|
857
983
|
npx @holdyapp/install <apiKey> [op\xE7\xF5es]
|
|
858
|
-
npx @holdyapp/install # modo interativo
|
|
859
984
|
HOLDY_API_KEY=hol_live_\u2026 npx @holdyapp/install # via env var
|
|
860
985
|
|
|
861
986
|
${bold("Argumentos:")}
|
|
@@ -868,6 +993,7 @@ ${bold("Op\xE7\xF5es:")}
|
|
|
868
993
|
--dry-run S\xF3 lista os eventos propostos \xB7 n\xE3o escreve nada (implica --instrument)
|
|
869
994
|
--max-events <n> Teto de eventos propostos \xB7 3 a 20 \xB7 default 12
|
|
870
995
|
--yes, -y Aplica o diff sem perguntar
|
|
996
|
+
--paste-key Pede a chave no terminal em vez de abrir o navegador
|
|
871
997
|
--api-url <url> Override da API (default https://holdy.chat/api/v1)
|
|
872
998
|
--version, -v Mostra vers\xE3o
|
|
873
999
|
--help, -h Mostra esta mensagem
|
|
@@ -884,7 +1010,8 @@ ${bold("Exemplos:")}
|
|
|
884
1010
|
|
|
885
1011
|
${bold("Sobre --instrument:")}
|
|
886
1012
|
Manda trechos do seu c\xF3digo pra Holdy, que devolve onde inserir cada track().
|
|
887
|
-
|
|
1013
|
+
Pelo navegador, a Holdy entrega uma chave secreta que vale 1 hora s\xF3 pra isso.
|
|
1014
|
+
Com chave colada, exige key SECRETA (n\xE3o a p\xFAblica do browser). Voc\xEA v\xEA o diff antes de aplicar,
|
|
888
1015
|
e nada \xE9 escrito sem confirma\xE7\xE3o. Pageview e rota j\xE1 s\xE3o autom\xE1ticos \u2014 o
|
|
889
1016
|
planner s\xF3 prop\xF5e momentos em que a conta avan\xE7a no produto.
|
|
890
1017
|
`);
|
package/package.json
CHANGED