@botozap/cli 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bytecraft Consultoria em Tecnologia LTDA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # @botozap/cli
2
+
3
+ CLI dev-first para a **API pública do BotoZap** — a plataforma multi-tenant sobre a
4
+ WhatsApp Cloud API oficial (a "Kapso brasileira"). Envie mensagens, gerencie contatos,
5
+ clientes, números, templates e webhooks direto do terminal ou de scripts.
6
+
7
+ A CLI é uma casca fina sobre o SDK oficial [`@botozap/sdk`](../sdk) — toda chamada de
8
+ rede passa por ele.
9
+
10
+ > Requer **Node ≥ 20.19**. Projeto usa **pnpm** (nunca npm/npx/yarn/bun).
11
+
12
+ > **Status: preview `0.x`, ainda não publicado no npm.** Sem promessa de
13
+ > estabilidade de comandos/flags até a `1.0`. A [API REST](https://botozap.com.br/docs)
14
+ > é a interface oficial e estável — se algo faltar na CLI, chame
15
+ > `https://botozap.com.br/api/v1` direto (`curl` + `Authorization: Bearer`).
16
+ > A CLI cobre hoje um SUBCONJUNTO da API: transmissões (`broadcasts`) e `flows`
17
+ > existem no SDK/REST, mas ainda não têm comando. Bugs:
18
+ > [issues do monorepo](https://github.com/bytecraft-fernando/botozap-js/issues)
19
+ > — nunca inclua sua chave de API no relato.
20
+
21
+ ## Instalação
22
+
23
+ Ainda **não publicado no npm** (em preparação). Por ora, use dentro do monorepo
24
+ `botozap-js`:
25
+
26
+ ```bash
27
+ # na raiz do monorepo
28
+ pnpm install
29
+ pnpm --filter @botozap/cli build
30
+ ```
31
+
32
+ Isso gera `packages/cli/dist/` com o binário `botozap` (campo `bin`). Para usar a partir
33
+ do checkout:
34
+
35
+ ```bash
36
+ # na pasta do pacote
37
+ cd packages/cli
38
+ pnpm link --global # disponibiliza o comando `botozap`
39
+ # ou rode direto:
40
+ node dist/index.js --help
41
+ ```
42
+
43
+ ## Autenticação
44
+
45
+ A CLI fala com `/api/v1` autenticando com o header `Authorization: Bearer bz_live_…`
46
+ (herdado do SDK). O servidor também aceita o legado `X-API-Key`, mas a CLI agora envia
47
+ **Bearer**. A chave é criada no painel do BotoZap, em **/chaves**.
48
+
49
+ A chave (e a URL base) são resolvidas nesta **ordem de prioridade**:
50
+
51
+ | Item | 1º (maior) | 2º | 3º | padrão |
52
+ | -------- | ----------------- | -------------------- | --------------------------- | ------------------------------- |
53
+ | API key | `--api-key` | `BOTOZAP_API_KEY` | `~/.botozap/cli/config.json`| — |
54
+ | URL base | `--api-url` | `BOTOZAP_API_URL` | `~/.botozap/cli/config.json`| `https://botozap.com.br/api/v1` |
55
+
56
+ > **Segurança:** prefira `BOTOZAP_API_KEY` (env) ou `botozap login` a passar
57
+ > `--api-key` na linha de comando — argumentos de CLI ficam no histórico do shell
58
+ > e visíveis em `ps`/argv. A flag existe para conveniência e CI, mas trata a chave
59
+ > como segredo em argv. O `config.json` é gravado com permissão `0600` (diretório `0700`).
60
+
61
+ ### Gravando a chave
62
+
63
+ ```bash
64
+ # stub amigável: explica /chaves e oferece colar a chave
65
+ botozap login
66
+
67
+ # direto
68
+ botozap config set apiKey bz_live_xxxxxxxxxxxx
69
+ botozap config set baseUrl https://botozap.com.br/api/v1
70
+
71
+ botozap config get # mostra config (apiKey mascarada)
72
+ botozap config path # caminho do config.json
73
+ ```
74
+
75
+ > **`botozap login` é um stub.** O BotoZap ainda não tem login OAuth por navegador
76
+ > (como `kapso login`); o comando apenas instrui a criar a chave em `/chaves` e grava
77
+ > a chave colada no config local.
78
+
79
+ ### Verificando
80
+
81
+ ```bash
82
+ botozap status
83
+ # Autenticado. Conta acessível.
84
+ # URL base https://botozap.com.br/api/v1
85
+ # Origem da chave config
86
+ # Números 3
87
+ ```
88
+
89
+ ## Saída: humano vs. JSON
90
+
91
+ Por padrão a saída é **humana** (tabelas/linhas compactas). Para scripts, use
92
+ `-o json` (ou `--output json`), que imprime o JSON **cru** da resposta:
93
+
94
+ ```bash
95
+ botozap messages list -o json | jq '.data[].id'
96
+ ```
97
+
98
+ > **Listas** preservam o envelope da API (`{ data, paging }` por cursor, `{ data, meta }`
99
+ > por offset). **Itens** (`get`, `create`, `update`, `media ingest`, `webhooks test`)
100
+ > saem **desembrulhados** — o objeto direto, sem a chave `data` em volta — porque os
101
+ > métodos de item do SDK já entregam a entidade. `messages send` sempre respondeu o
102
+ > objeto direto (a rota `POST /messages` não usa envelope).
103
+
104
+ ## Comandos
105
+
106
+ ```
107
+ botozap messages send | list | get
108
+ botozap conversations list | get | update
109
+ botozap contacts list | get | create | update | delete
110
+ botozap media ingest
111
+ botozap customers list | get | create | update | delete
112
+ botozap setup-links list | create | update (--customer <id>)
113
+ botozap numbers list | get | health
114
+ botozap templates list | get | create
115
+ botozap webhooks list | get | create | update | delete | test
116
+ botozap deliveries list (webhook_deliveries)
117
+ botozap logs list (api_logs)
118
+ botozap users list
119
+ botozap config set | get | path
120
+ botozap login
121
+ botozap status
122
+ ```
123
+
124
+ ### Flags globais
125
+
126
+ - `--api-key <chave>` / `--api-url <url>` — sobrepõem env e config.
127
+ - `-o, --output <human|json>` — formato de saída (padrão `human`).
128
+ - Listas por **cursor**: `--limit`, `--after`, `--before`.
129
+ - Listas por **offset**: `--page`, `--per-page`.
130
+
131
+ ### Exemplos
132
+
133
+ ```bash
134
+ # Enviar texto simples
135
+ botozap messages send --to 5511999999999 --text "Olá do BotoZap!"
136
+
137
+ # Enviar payload completo (template, mídia, etc.) via arquivo ou stdin
138
+ botozap messages send --input ./mensagem.json
139
+ cat ./mensagem.json | botozap messages send --stdin
140
+
141
+ # Listar mensagens com filtro e paginação por cursor
142
+ botozap messages list --direction inbound --limit 20
143
+ botozap messages list --after <cursor>
144
+
145
+ # Encerrar uma conversa
146
+ botozap conversations update <id> --status ended
147
+
148
+ # Criar contato
149
+ botozap contacts create --wa-id 5511999999999 --profile-name "Maria"
150
+
151
+ # Ingerir mídia por URL
152
+ botozap media ingest --phone-number-id <id> --source https://exemplo.com/foto.jpg
153
+
154
+ # Clientes (offset)
155
+ botozap customers list --page 1 --per-page 25
156
+ botozap customers create --name "Acme LTDA" --external-customer-id acme-001
157
+
158
+ # Setup links de um cliente
159
+ botozap setup-links list --customer <customerId>
160
+ botozap setup-links create --customer <customerId>
161
+
162
+ # Números e saúde
163
+ botozap numbers list
164
+ botozap numbers health <phoneNumberId>
165
+
166
+ # Templates (componentes via arquivo JSON)
167
+ botozap templates create --name boas_vindas --language pt_BR \
168
+ --category UTILITY --components ./components.json
169
+
170
+ # Webhooks (eventos válidos: messages, statuses)
171
+ botozap webhooks create --url https://meu.app/webhook \
172
+ --events messages,statuses --secret s3cr3t
173
+ botozap webhooks test <id>
174
+
175
+ # Entregas e logs
176
+ botozap deliveries list --status failed
177
+ botozap logs list --status-code 500
178
+ ```
179
+
180
+ ## Tratamento de erros
181
+
182
+ A API usa o envelope `{ "error": { "code", "message" } }`. A CLI imprime a `message`
183
+ (em PT-BR) no **stderr** e sai com **código 1**:
184
+
185
+ ```
186
+ Erro [authentication_error]: Chave de API inválida ou expirada.
187
+ ```
188
+
189
+ Num `429 rate_limited`, a CLI acrescenta o `Retry-After` (e `X-RateLimit-*`,
190
+ quando presentes) à mensagem — espere esse tempo antes de reenviar.
191
+
192
+ Com `-o json`, erros saem **estruturados no stderr** (stdout fica limpo para
193
+ pipes): `{ "error": { "code", "message", "status", "rate_limit"? } }`. O exit
194
+ code é **1** para qualquer erro (`0` só no sucesso) — trate classes de erro pelo
195
+ `code` do JSON, não pelo exit code.
196
+
197
+ ## Estrutura
198
+
199
+ ```
200
+ src/
201
+ index.ts # entrada (bin), parsing com commander, tratamento de erro
202
+ client.ts # resolve config e instancia `BotoZap` de @botozap/sdk
203
+ config.ts # leitura/escrita de ~/.botozap/cli/config.json + resolução
204
+ output.ts # formatação human/json (tabelas, detalhes, paginação)
205
+ commands/*.ts # um arquivo por recurso
206
+ ```
207
+
208
+ ## Desenvolvimento
209
+
210
+ ```bash
211
+ pnpm --filter @botozap/cli typecheck # tsc --noEmit
212
+ pnpm --filter @botozap/cli test # vitest
213
+ pnpm --filter @botozap/cli build # tsc -> dist/
214
+ node packages/cli/dist/index.js --help
215
+ ```
package/dist/client.js ADDED
@@ -0,0 +1,24 @@
1
+ import { BotoZap } from "@botozap/sdk";
2
+ import { resolveAuth } from "./config.js";
3
+ // `BotoZapError` (erro de API: `.code`/`.message`/`.status`) vem do SDK — a CLI
4
+ // não tem mais cliente HTTP próprio. Re-exporta para o handler central de erro.
5
+ export { BotoZapError } from "@botozap/sdk";
6
+ /** Erro de configuração local (ex.: falta de API key). Não sai do SDK. */
7
+ export class ConfigError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "ConfigError";
11
+ }
12
+ }
13
+ /**
14
+ * Resolve credenciais (flag > env > config) e instancia o cliente do SDK.
15
+ * A ausência de chave é um erro LOCAL (`ConfigError`) — nunca chega a rede.
16
+ */
17
+ export function createClient(global) {
18
+ const auth = resolveAuth({ apiKey: global.apiKey, apiUrl: global.apiUrl });
19
+ if (!auth.apiKey) {
20
+ throw new ConfigError("Nenhuma API key encontrada. Defina com `botozap config set apiKey bz_live_…`, " +
21
+ "exporte BOTOZAP_API_KEY ou passe --api-key. Crie uma chave em /chaves no painel.");
22
+ }
23
+ return new BotoZap({ apiKey: auth.apiKey, baseUrl: auth.baseUrl });
24
+ }
@@ -0,0 +1,53 @@
1
+ import { readConfig, setConfigValue, configPath, } from "../config.js";
2
+ import { resolveFormat, printJson, printLine, dim } from "../output.js";
3
+ const ALLOWED_KEYS = ["apiKey", "baseUrl"];
4
+ /** Mascara a API key, mostrando só o prefixo e os últimos 4 caracteres. */
5
+ function maskKey(key) {
6
+ if (!key)
7
+ return "(não definida)";
8
+ if (key.length <= 12)
9
+ return key.slice(0, 4) + "…";
10
+ return key.slice(0, 8) + "…" + key.slice(-4);
11
+ }
12
+ export function registerConfig(program) {
13
+ const config = program
14
+ .command("config")
15
+ .description("Ler e gravar a configuração local da CLI");
16
+ config
17
+ .command("set <chave> <valor>")
18
+ .description("Define uma chave de config (apiKey | baseUrl)")
19
+ .action((key, value) => {
20
+ if (!ALLOWED_KEYS.includes(key)) {
21
+ throw new Error(`Chave inválida: "${key}". Use uma de: ${ALLOWED_KEYS.join(", ")}.`);
22
+ }
23
+ setConfigValue(key, value);
24
+ printLine(`Gravado em ${configPath()}`);
25
+ if (key === "apiKey") {
26
+ printLine(dim(`apiKey = ${maskKey(value)}`));
27
+ }
28
+ else {
29
+ printLine(dim(`${key} = ${value}`));
30
+ }
31
+ });
32
+ config
33
+ .command("get")
34
+ .description("Mostra a config atual (apiKey mascarada)")
35
+ .action((_opts, cmd) => {
36
+ const format = resolveFormat(cmd.optsWithGlobals().output);
37
+ const stored = readConfig();
38
+ if (format === "json") {
39
+ return printJson({
40
+ apiKey: stored.apiKey ? maskKey(stored.apiKey) : null,
41
+ baseUrl: stored.baseUrl ?? null,
42
+ });
43
+ }
44
+ printLine(`apiKey ${maskKey(stored.apiKey)}`);
45
+ printLine(`baseUrl ${stored.baseUrl ?? dim("(padrão)")}`);
46
+ });
47
+ config
48
+ .command("path")
49
+ .description("Mostra o caminho do arquivo de configuração")
50
+ .action(() => {
51
+ printLine(configPath());
52
+ });
53
+ }
@@ -0,0 +1,110 @@
1
+ import { context, toInt, toBool } from "./shared.js";
2
+ import { printJson, printTable, printDetail, printCursorFooter, printLine, } from "../output.js";
3
+ export function registerContacts(program) {
4
+ const contacts = program
5
+ .command("contacts")
6
+ .description("Gerenciar contatos");
7
+ contacts
8
+ .command("list")
9
+ .description("Lista contatos (paginação por cursor)")
10
+ .option("--customer-id <id>", "filtra por cliente")
11
+ .option("--profile-name-contains <texto>", "filtra por nome do perfil")
12
+ .option("--wa-id-contains <texto>", "filtra por wa_id")
13
+ .option("--has-customer <bool>", "true | false")
14
+ .option("--created-after <data>", "ISO 8601")
15
+ .option("--created-before <data>", "ISO 8601")
16
+ .option("--limit <n>", "quantidade por página")
17
+ .option("--after <cursor>", "cursor da próxima página")
18
+ .option("--before <cursor>", "cursor da página anterior")
19
+ .action(async (opts, cmd) => {
20
+ const { client, format } = context(cmd);
21
+ const res = await client.contacts.list({
22
+ customer_id: opts.customerId,
23
+ profile_name_contains: opts.profileNameContains,
24
+ wa_id_contains: opts.waIdContains,
25
+ has_customer: toBool(opts.hasCustomer),
26
+ created_after: opts.createdAfter,
27
+ created_before: opts.createdBefore,
28
+ limit: toInt(opts.limit),
29
+ after: opts.after,
30
+ before: opts.before,
31
+ });
32
+ if (format === "json")
33
+ return printJson(res);
34
+ printTable(res.data, [
35
+ { header: "ID", key: "id", max: 36 },
36
+ { header: "WA_ID", key: "wa_id" },
37
+ { header: "NOME", key: "profile_name" },
38
+ { header: "USERNAME", key: "username" },
39
+ { header: "CLIENTE", key: "customer_id", max: 36 },
40
+ ]);
41
+ printCursorFooter(res.paging);
42
+ });
43
+ contacts
44
+ .command("get <id>")
45
+ .description("Detalha um contato")
46
+ .action(async (id, _opts, cmd) => {
47
+ const { client, format } = context(cmd);
48
+ const data = await client.contacts.get(id);
49
+ if (format === "json")
50
+ return printJson(data);
51
+ printDetail(data);
52
+ });
53
+ contacts
54
+ .command("create")
55
+ .description("Cria um contato")
56
+ .requiredOption("--wa-id <wa_id>", "WhatsApp ID (obrigatório)")
57
+ .option("--profile-name <nome>", "nome do perfil")
58
+ .option("--phone <telefone>", "telefone")
59
+ .option("--user-id <id>", "id externo do usuário")
60
+ .option("--username <username>", "username")
61
+ .option("--customer-id <id>", "cliente vinculado")
62
+ .option("--phone-number-id <id>", "número vinculado")
63
+ .action(async (opts, cmd) => {
64
+ const { client, format } = context(cmd);
65
+ const data = await client.contacts.create({
66
+ wa_id: opts.waId,
67
+ profile_name: opts.profileName,
68
+ phone: opts.phone,
69
+ user_id: opts.userId,
70
+ username: opts.username,
71
+ customer_id: opts.customerId,
72
+ phone_number_id: opts.phoneNumberId,
73
+ });
74
+ if (format === "json")
75
+ return printJson(data);
76
+ printLine("Contato criado.");
77
+ printDetail(data);
78
+ });
79
+ contacts
80
+ .command("update <id>")
81
+ .description("Atualiza um contato")
82
+ .option("--profile-name <nome>", "novo nome do perfil")
83
+ .option("--username <username>", "novo username")
84
+ .action(async (id, opts, cmd) => {
85
+ const { client, format } = context(cmd);
86
+ const body = {};
87
+ if (opts.profileName !== undefined)
88
+ body.profile_name = opts.profileName;
89
+ if (opts.username !== undefined)
90
+ body.username = opts.username;
91
+ if (Object.keys(body).length === 0) {
92
+ throw new Error("Informe ao menos --profile-name ou --username.");
93
+ }
94
+ const data = await client.contacts.update(id, body);
95
+ if (format === "json")
96
+ return printJson(data);
97
+ printLine("Contato atualizado.");
98
+ printDetail(data);
99
+ });
100
+ contacts
101
+ .command("delete <id>")
102
+ .description("Remove um contato")
103
+ .action(async (id, _opts, cmd) => {
104
+ const { client, format } = context(cmd);
105
+ await client.contacts.delete(id);
106
+ if (format === "json")
107
+ return printJson({ deleted: true });
108
+ printLine("Contato removido.");
109
+ });
110
+ }
@@ -0,0 +1,60 @@
1
+ import { context, toInt } from "./shared.js";
2
+ import { printJson, printTable, printDetail, printCursorFooter, printLine, } from "../output.js";
3
+ export function registerConversations(program) {
4
+ const conv = program
5
+ .command("conversations")
6
+ .description("Listar e gerenciar conversas");
7
+ conv
8
+ .command("list")
9
+ .description("Lista conversas (paginação por cursor)")
10
+ .option("--phone-number-id <id>", "filtra por número")
11
+ .option("--status <status>", "active | ended")
12
+ .option("--phone-number <e164>", "filtra pelo número do contato")
13
+ .option("--limit <n>", "quantidade por página")
14
+ .option("--after <cursor>", "cursor da próxima página")
15
+ .option("--before <cursor>", "cursor da página anterior")
16
+ .action(async (opts, cmd) => {
17
+ const { client, format } = context(cmd);
18
+ const res = await client.conversations.list({
19
+ phone_number_id: opts.phoneNumberId,
20
+ status: opts.status,
21
+ phone_number: opts.phoneNumber,
22
+ limit: toInt(opts.limit),
23
+ after: opts.after,
24
+ before: opts.before,
25
+ });
26
+ if (format === "json")
27
+ return printJson(res);
28
+ printTable(res.data, [
29
+ { header: "ID", key: "id", max: 36 },
30
+ { header: "CONTATO", key: "phone_number" },
31
+ { header: "STATUS", key: "status" },
32
+ { header: "ATUALIZADA", key: "updated_at" },
33
+ ]);
34
+ printCursorFooter(res.paging);
35
+ });
36
+ conv
37
+ .command("get <id>")
38
+ .description("Detalha uma conversa")
39
+ .action(async (id, _opts, cmd) => {
40
+ const { client, format } = context(cmd);
41
+ const data = await client.conversations.get(id);
42
+ if (format === "json")
43
+ return printJson(data);
44
+ printDetail(data);
45
+ });
46
+ conv
47
+ .command("update <id>")
48
+ .description("Atualiza o status de uma conversa (active | ended)")
49
+ .requiredOption("--status <status>", "active | ended")
50
+ .action(async (id, opts, cmd) => {
51
+ const { client, format } = context(cmd);
52
+ const data = await client.conversations.update(id, {
53
+ status: opts.status,
54
+ });
55
+ if (format === "json")
56
+ return printJson(data);
57
+ printLine("Conversa atualizada.");
58
+ printDetail(data);
59
+ });
60
+ }
@@ -0,0 +1,86 @@
1
+ import { context, toInt } from "./shared.js";
2
+ import { printJson, printTable, printDetail, printOffsetFooter, printLine, } from "../output.js";
3
+ export function registerCustomers(program) {
4
+ const customers = program
5
+ .command("customers")
6
+ .description("Gerenciar clientes (do seu workspace)");
7
+ customers
8
+ .command("list")
9
+ .description("Lista clientes (paginação por offset)")
10
+ .option("--page <n>", "página")
11
+ .option("--per-page <n>", "itens por página")
12
+ .action(async (opts, cmd) => {
13
+ const { client, format } = context(cmd);
14
+ const res = await client.customers.list({
15
+ page: toInt(opts.page),
16
+ per_page: toInt(opts.perPage),
17
+ });
18
+ if (format === "json")
19
+ return printJson(res);
20
+ printTable(res.data, [
21
+ { header: "ID", key: "id", max: 36 },
22
+ { header: "NOME", key: "name" },
23
+ { header: "EXTERNO", key: "external_customer_id" },
24
+ { header: "CRIADO", key: "created_at" },
25
+ ]);
26
+ printOffsetFooter(res.meta);
27
+ });
28
+ customers
29
+ .command("get <id>")
30
+ .description("Detalha um cliente")
31
+ .action(async (id, _opts, cmd) => {
32
+ const { client, format } = context(cmd);
33
+ const data = await client.customers.get(id);
34
+ if (format === "json")
35
+ return printJson(data);
36
+ printDetail(data);
37
+ });
38
+ customers
39
+ .command("create")
40
+ .description("Cria um cliente")
41
+ .requiredOption("--name <nome>", "nome do cliente (obrigatório)")
42
+ .option("--external-customer-id <id>", "id externo")
43
+ .action(async (opts, cmd) => {
44
+ const { client, format } = context(cmd);
45
+ const data = await client.customers.create({
46
+ name: opts.name,
47
+ external_customer_id: opts.externalCustomerId,
48
+ });
49
+ if (format === "json")
50
+ return printJson(data);
51
+ printLine("Cliente criado.");
52
+ printDetail(data);
53
+ });
54
+ customers
55
+ .command("update <id>")
56
+ .description("Atualiza um cliente")
57
+ .option("--name <nome>", "novo nome")
58
+ .option("--external-customer-id <id>", "novo id externo")
59
+ .action(async (id, opts, cmd) => {
60
+ const { client, format } = context(cmd);
61
+ const body = {};
62
+ if (opts.name !== undefined)
63
+ body.name = opts.name;
64
+ if (opts.externalCustomerId !== undefined) {
65
+ body.external_customer_id = opts.externalCustomerId;
66
+ }
67
+ if (Object.keys(body).length === 0) {
68
+ throw new Error("Informe ao menos --name ou --external-customer-id.");
69
+ }
70
+ const data = await client.customers.update(id, body);
71
+ if (format === "json")
72
+ return printJson(data);
73
+ printLine("Cliente atualizado.");
74
+ printDetail(data);
75
+ });
76
+ customers
77
+ .command("delete <id>")
78
+ .description("Remove um cliente")
79
+ .action(async (id, _opts, cmd) => {
80
+ const { client, format } = context(cmd);
81
+ await client.customers.delete(id);
82
+ if (format === "json")
83
+ return printJson({ deleted: true });
84
+ printLine("Cliente removido.");
85
+ });
86
+ }
@@ -0,0 +1,40 @@
1
+ import { context, toInt } from "./shared.js";
2
+ import { printJson, printTable, printCursorFooter } from "../output.js";
3
+ export function registerDeliveries(program) {
4
+ const deliveries = program
5
+ .command("deliveries")
6
+ .description("Entregas de webhook (webhook_deliveries)");
7
+ deliveries
8
+ .command("list")
9
+ .description("Lista entregas de webhook (paginação por cursor)")
10
+ .option("--endpoint-id <id>", "filtra por endpoint")
11
+ .option("--webhook-id <id>", "alias de --endpoint-id")
12
+ .option("--status <status>", "filtra por status")
13
+ .option("--event-type <tipo>", "filtra por tipo de evento")
14
+ .option("--limit <n>", "quantidade por página")
15
+ .option("--after <cursor>", "cursor da próxima página")
16
+ .option("--before <cursor>", "cursor da página anterior")
17
+ .action(async (opts, cmd) => {
18
+ const { client, format } = context(cmd);
19
+ // `webhook_id` é alias de `endpoint_id` na rota; --endpoint-id/--webhook-id
20
+ // colapsam no mesmo param (endpoint tem precedência).
21
+ const res = await client.webhookDeliveries.list({
22
+ webhook_id: opts.endpointId ?? opts.webhookId,
23
+ status: opts.status,
24
+ event_type: opts.eventType,
25
+ limit: toInt(opts.limit),
26
+ after: opts.after,
27
+ before: opts.before,
28
+ });
29
+ if (format === "json")
30
+ return printJson(res);
31
+ printTable(res.data, [
32
+ { header: "ID", key: "id", max: 36 },
33
+ { header: "EVENTO", key: "event_type" },
34
+ { header: "STATUS", key: "status" },
35
+ { header: "HTTP", key: "response_status" },
36
+ { header: "CRIADA", key: "created_at" },
37
+ ]);
38
+ printCursorFooter(res.paging);
39
+ });
40
+ }
@@ -0,0 +1,61 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin, stdout } from "node:process";
3
+ import { setConfigValue, configPath } from "../config.js";
4
+ import { printLine, dim } from "../output.js";
5
+ import pc from "picocolors";
6
+ /**
7
+ * `botozap login` — STUB amigável.
8
+ *
9
+ * O BotoZap ainda não tem login OAuth por navegador (como o `kapso login`).
10
+ * Por isso este comando apenas (1) explica como criar uma API key no painel,
11
+ * em /chaves, e (2) oferece colar a chave para gravá-la no config local.
12
+ */
13
+ export function registerLogin(program) {
14
+ program
15
+ .command("login")
16
+ .description("Instruções para autenticar e gravar sua API key (stub)")
17
+ .option("--api-key <chave>", "grava a chave informada sem perguntar")
18
+ .action(async (opts, _cmd) => {
19
+ printLine(pc.bold("Autenticação do BotoZap CLI"));
20
+ printLine("");
21
+ printLine("Ainda não há login por navegador (OAuth). Use uma API key do painel:");
22
+ printLine(` 1. Acesse ${pc.cyan("https://botozap.com.br/chaves")}`);
23
+ printLine(" 2. Crie uma chave (formato bz_live_…)");
24
+ printLine(" 3. Cole a chave abaixo (ou rode com --api-key).");
25
+ printLine("");
26
+ let key = opts.apiKey;
27
+ if (!key) {
28
+ const interativo = Boolean(stdin.isTTY);
29
+ const rl = createInterface({ input: stdin, output: stdout });
30
+ try {
31
+ const pergunta = rl.question("Cole sua API key: ");
32
+ if (interativo) {
33
+ // Mascara a digitação: o prompt acima já foi impresso, então a
34
+ // partir daqui suprimimos o eco de cada caractere da chave. Assim
35
+ // ela não aparece na tela nem fica no scrollback do terminal.
36
+ // (Num pipe/redirect não há eco de terminal, então não mascaramos —
37
+ // o `botozap login < chave.txt` / `echo … | botozap login` funciona.)
38
+ rl._writeToOutput = () => { };
39
+ }
40
+ key = (await pergunta).trim();
41
+ // O Enter final também foi suprimido pela máscara: reponha a quebra.
42
+ if (interativo)
43
+ stdout.write("\n");
44
+ }
45
+ finally {
46
+ rl.close();
47
+ }
48
+ }
49
+ if (!key) {
50
+ printLine(dim("Nenhuma chave informada. Nada foi gravado."));
51
+ return;
52
+ }
53
+ if (!key.startsWith("bz_")) {
54
+ printLine(pc.yellow("Aviso: a chave não começa com `bz_`. Gravando mesmo assim."));
55
+ }
56
+ setConfigValue("apiKey", key);
57
+ printLine(pc.green("Chave gravada com sucesso."));
58
+ printLine(dim(`Arquivo: ${configPath()}`));
59
+ printLine(dim("Verifique com `botozap status`."));
60
+ });
61
+ }