@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.
@@ -0,0 +1,38 @@
1
+ import { context, toInt } from "./shared.js";
2
+ import { printJson, printTable, printCursorFooter } from "../output.js";
3
+ export function registerLogs(program) {
4
+ const logs = program
5
+ .command("logs")
6
+ .description("Logs de requisições à API (api_logs)");
7
+ logs
8
+ .command("list")
9
+ .description("Lista logs de API (paginação por cursor)")
10
+ .option("--source <fonte>", "filtra por fonte")
11
+ .option("--method <metodo>", "filtra por método HTTP")
12
+ .option("--status-code <codigo>", "filtra por status HTTP")
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.apiLogs.list({
19
+ source: opts.source,
20
+ method: opts.method,
21
+ status_code: toInt(opts.statusCode),
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: "MÉTODO", key: "method" },
31
+ { header: "ROTA", key: "path", max: 40 },
32
+ { header: "STATUS", key: "status_code" },
33
+ { header: "FONTE", key: "source" },
34
+ { header: "CRIADO", key: "created_at" },
35
+ ]);
36
+ printCursorFooter(res.paging);
37
+ });
38
+ }
@@ -0,0 +1,28 @@
1
+ import { context } from "./shared.js";
2
+ import { printJson, printDetail, printLine } from "../output.js";
3
+ export function registerMedia(program) {
4
+ const media = program.command("media").description("Ingestão de mídia");
5
+ media
6
+ .command("ingest")
7
+ .description("Ingere uma mídia a partir de uma URL")
8
+ .requiredOption("--phone-number-id <id>", "número de origem (obrigatório)")
9
+ .requiredOption("--source <url>", "URL pública da mídia (obrigatório)")
10
+ .option("--filename <nome>", "nome do arquivo")
11
+ .option("--mime-type <tipo>", "tipo MIME (ex.: image/jpeg)")
12
+ .option("--delivery <modo>", "modo de entrega")
13
+ .action(async (opts, cmd) => {
14
+ const { client, format } = context(cmd);
15
+ const data = await client.media.upload({
16
+ phone_number_id: opts.phoneNumberId,
17
+ source: opts.source,
18
+ filename: opts.filename,
19
+ mime_type: opts.mimeType,
20
+ // A rota valida o valor; aqui só repassamos a string do flag.
21
+ delivery: opts.delivery,
22
+ });
23
+ if (format === "json")
24
+ return printJson(data);
25
+ printLine("Mídia ingerida.");
26
+ printDetail(data);
27
+ });
28
+ }
@@ -0,0 +1,113 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { context, toInt } from "./shared.js";
3
+ import { printJson, printTable, printDetail, printCursorFooter, printLine, } from "../output.js";
4
+ /** Lê JSON cru de um arquivo (--input) ou do stdin (--stdin). */
5
+ function readRawBody(file, fromStdin) {
6
+ let raw;
7
+ if (file) {
8
+ raw = readFileSync(file, "utf8");
9
+ }
10
+ else if (fromStdin) {
11
+ raw = readFileSync(0, "utf8");
12
+ }
13
+ else {
14
+ throw new Error("Nenhuma fonte de payload informada.");
15
+ }
16
+ try {
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ throw new Error("O payload informado não é um JSON válido.");
21
+ }
22
+ }
23
+ export function registerMessages(program) {
24
+ const messages = program
25
+ .command("messages")
26
+ .description("Enviar e consultar mensagens de WhatsApp");
27
+ messages
28
+ .command("send")
29
+ .description("Envia uma mensagem (texto simples ou payload completo)")
30
+ .option("--to <wa_id>", "destinatário (número E.164 sem +, ex.: 5511999999999)")
31
+ .option("--text <body>", "corpo de uma mensagem de texto")
32
+ .option("--from <phone_number_id>", "número de origem (opcional)")
33
+ .option("--input <arquivo>", "arquivo JSON com o corpo bruto da mensagem")
34
+ .option("--stdin", "lê o corpo bruto da mensagem do stdin")
35
+ .action(async (opts, cmd) => {
36
+ const { client, format } = context(cmd);
37
+ // POST /messages responde o objeto DIRETO (sem envelope `data`); ambos os
38
+ // caminhos devolvem o mesmo shape { id, wamid, to, status }.
39
+ let result;
40
+ if (opts.input || opts.stdin) {
41
+ // Payload cru: contrato de baixo nível do SDK (`request`), sem montar o corpo.
42
+ const body = readRawBody(opts.input, opts.stdin);
43
+ result = await client.request("POST", "/messages", { body });
44
+ }
45
+ else {
46
+ if (!opts.to || !opts.text) {
47
+ throw new Error("Informe --to e --text, ou use --input <arquivo.json> / --stdin para payload completo.");
48
+ }
49
+ result = await client.messages.send({
50
+ to: opts.to,
51
+ text: opts.text,
52
+ ...(opts.from ? { from: opts.from } : {}),
53
+ });
54
+ }
55
+ if (format === "json")
56
+ return printJson(result);
57
+ printLine("Mensagem enfileirada.");
58
+ printDetail(result, [
59
+ "id",
60
+ "wamid",
61
+ "to",
62
+ "status",
63
+ ]);
64
+ });
65
+ messages
66
+ .command("list")
67
+ .description("Lista mensagens (paginação por cursor)")
68
+ .option("--phone-number-id <id>", "filtra por número")
69
+ .option("--conversation-id <id>", "filtra por conversa")
70
+ .option("--direction <dir>", "inbound | outbound")
71
+ .option("--status <status>", "status da mensagem")
72
+ .option("--message-type <tipo>", "tipo (text, image, template, …)")
73
+ .option("--has-media", "apenas mensagens com mídia")
74
+ .option("--limit <n>", "quantidade por página")
75
+ .option("--after <cursor>", "cursor da próxima página")
76
+ .option("--before <cursor>", "cursor da página anterior")
77
+ .action(async (opts, cmd) => {
78
+ const { client, format } = context(cmd);
79
+ const res = await client.messages.list({
80
+ phone_number_id: opts.phoneNumberId,
81
+ conversation_id: opts.conversationId,
82
+ direction: opts.direction,
83
+ status: opts.status,
84
+ message_type: opts.messageType,
85
+ has_media: opts.hasMedia ? true : undefined,
86
+ limit: toInt(opts.limit),
87
+ after: opts.after,
88
+ before: opts.before,
89
+ });
90
+ if (format === "json")
91
+ return printJson(res);
92
+ printTable(res.data, [
93
+ { header: "ID", key: "id", max: 36 },
94
+ { header: "DIREÇÃO", key: "direction" },
95
+ { header: "TIPO", key: "kind" },
96
+ { header: "STATUS", key: "status" },
97
+ { header: "DE", key: "from" },
98
+ { header: "PARA", key: "to" },
99
+ { header: "CRIADA", key: "created_at" },
100
+ ]);
101
+ printCursorFooter(res.paging);
102
+ });
103
+ messages
104
+ .command("get <id>")
105
+ .description("Detalha uma mensagem por UUID interno ou wamid")
106
+ .action(async (id, _opts, cmd) => {
107
+ const { client, format } = context(cmd);
108
+ const data = await client.messages.get(id);
109
+ if (format === "json")
110
+ return printJson(data);
111
+ printDetail(data);
112
+ });
113
+ }
@@ -0,0 +1,51 @@
1
+ import { context, toInt } from "./shared.js";
2
+ import { printJson, printTable, printDetail, printOffsetFooter, } from "../output.js";
3
+ export function registerNumbers(program) {
4
+ const numbers = program
5
+ .command("numbers")
6
+ .description("Números de telefone (WhatsApp) conectados");
7
+ numbers
8
+ .command("list")
9
+ .description("Lista números (paginação por offset)")
10
+ .option("--customer-id <id>", "filtra por cliente")
11
+ .option("--page <n>", "página")
12
+ .option("--per-page <n>", "itens por página")
13
+ .action(async (opts, cmd) => {
14
+ const { client, format } = context(cmd);
15
+ const res = await client.phoneNumbers.list({
16
+ customer_id: opts.customerId,
17
+ page: toInt(opts.page),
18
+ per_page: toInt(opts.perPage),
19
+ });
20
+ if (format === "json")
21
+ return printJson(res);
22
+ printTable(res.data, [
23
+ { header: "ID", key: "id", max: 36 },
24
+ { header: "NÚMERO", key: "display_phone_number" },
25
+ { header: "NOME", key: "verified_name" },
26
+ { header: "STATUS", key: "status" },
27
+ { header: "QUALIDADE", key: "quality_rating" },
28
+ ]);
29
+ printOffsetFooter(res.meta);
30
+ });
31
+ numbers
32
+ .command("get <id>")
33
+ .description("Detalha um número")
34
+ .action(async (id, _opts, cmd) => {
35
+ const { client, format } = context(cmd);
36
+ const data = await client.phoneNumbers.get(id);
37
+ if (format === "json")
38
+ return printJson(data);
39
+ printDetail(data);
40
+ });
41
+ numbers
42
+ .command("health <id>")
43
+ .description("Verifica a saúde de um número")
44
+ .action(async (id, _opts, cmd) => {
45
+ const { client, format } = context(cmd);
46
+ const data = await client.phoneNumbers.health(id);
47
+ if (format === "json")
48
+ return printJson(data);
49
+ printDetail(data);
50
+ });
51
+ }
@@ -0,0 +1,63 @@
1
+ import { context, toInt } from "./shared.js";
2
+ import { printJson, printTable, printDetail, printOffsetFooter, printLine, } from "../output.js";
3
+ export function registerSetupLinks(program) {
4
+ const links = program
5
+ .command("setup-links")
6
+ .description("Links de configuração (Embedded Signup) de um cliente");
7
+ links
8
+ .command("list")
9
+ .description("Lista os setup links de um cliente (offset)")
10
+ .requiredOption("--customer <id>", "id do cliente (obrigatório)")
11
+ .option("--page <n>", "página")
12
+ .option("--per-page <n>", "itens por página")
13
+ .action(async (opts, cmd) => {
14
+ const { client, format } = context(cmd);
15
+ const res = await client.customers.listSetupLinks(opts.customer, {
16
+ page: toInt(opts.page),
17
+ per_page: toInt(opts.perPage),
18
+ });
19
+ if (format === "json")
20
+ return printJson(res);
21
+ printTable(res.data, [
22
+ { header: "ID", key: "id", max: 36 },
23
+ { header: "STATUS", key: "status" },
24
+ { header: "URL", key: "url", max: 60 },
25
+ { header: "EXPIRA", key: "expires_at" },
26
+ ]);
27
+ printOffsetFooter(res.meta);
28
+ });
29
+ links
30
+ .command("create")
31
+ .description("Cria um setup link para um cliente")
32
+ .requiredOption("--customer <id>", "id do cliente (obrigatório)")
33
+ .action(async (opts, cmd) => {
34
+ const { client, format } = context(cmd);
35
+ const data = await client.customers.createSetupLink(opts.customer);
36
+ if (format === "json")
37
+ return printJson(data);
38
+ printLine("Setup link criado.");
39
+ printDetail(data);
40
+ });
41
+ links
42
+ .command("update <linkId>")
43
+ .description("Atualiza um setup link (status / expiração)")
44
+ .requiredOption("--customer <id>", "id do cliente (obrigatório)")
45
+ .option("--status <status>", "novo status")
46
+ .option("--expires-at <data>", "nova expiração (ISO 8601)")
47
+ .action(async (linkId, opts, cmd) => {
48
+ const { client, format } = context(cmd);
49
+ const body = {};
50
+ if (opts.status !== undefined)
51
+ body.status = opts.status;
52
+ if (opts.expiresAt !== undefined)
53
+ body.expires_at = opts.expiresAt;
54
+ if (Object.keys(body).length === 0) {
55
+ throw new Error("Informe ao menos --status ou --expires-at.");
56
+ }
57
+ const data = await client.customers.updateSetupLink(opts.customer, linkId, body);
58
+ if (format === "json")
59
+ return printJson(data);
60
+ printLine("Setup link atualizado.");
61
+ printDetail(data);
62
+ });
63
+ }
@@ -0,0 +1,37 @@
1
+ import { createClient } from "../client.js";
2
+ import { resolveFormat } from "../output.js";
3
+ /** Lê as flags globais (--api-key/--api-url/--output) de qualquer subcomando. */
4
+ export function readGlobals(cmd) {
5
+ const opts = cmd.optsWithGlobals();
6
+ return {
7
+ apiKey: opts.apiKey,
8
+ apiUrl: opts.apiUrl,
9
+ output: opts.output,
10
+ };
11
+ }
12
+ /** Constrói client (SDK) + formato a partir do comando atual. */
13
+ export function context(cmd) {
14
+ const g = readGlobals(cmd);
15
+ const global = { apiKey: g.apiKey, apiUrl: g.apiUrl };
16
+ return {
17
+ client: createClient(global),
18
+ format: resolveFormat(g.output),
19
+ };
20
+ }
21
+ /** Converte string para inteiro, ou undefined se vazio/ inválido. */
22
+ export function toInt(value) {
23
+ if (value === undefined)
24
+ return undefined;
25
+ const n = Number.parseInt(value, 10);
26
+ return Number.isNaN(n) ? undefined : n;
27
+ }
28
+ /** Booleano a partir de string "true"/"false" (ou undefined). */
29
+ export function toBool(value) {
30
+ if (value === undefined)
31
+ return undefined;
32
+ if (value === "true")
33
+ return true;
34
+ if (value === "false")
35
+ return false;
36
+ return undefined;
37
+ }
@@ -0,0 +1,32 @@
1
+ import { createClient } from "../client.js";
2
+ import { resolveAuth } from "../config.js";
3
+ import { resolveFormat, printJson, printLine, dim } from "../output.js";
4
+ import pc from "picocolors";
5
+ export function registerStatus(program) {
6
+ program
7
+ .command("status")
8
+ .description("Verifica a autenticação e a conectividade com a API")
9
+ .action(async (_opts, cmd) => {
10
+ const g = cmd.optsWithGlobals();
11
+ const format = resolveFormat(g.output);
12
+ const auth = resolveAuth({ apiKey: g.apiKey, apiUrl: g.apiUrl });
13
+ const global = { apiKey: g.apiKey, apiUrl: g.apiUrl };
14
+ const client = createClient(global);
15
+ // Sonda leve: 1 número apenas confirma que a chave alcança a conta.
16
+ const res = await client.phoneNumbers.list({ per_page: 1 });
17
+ const total = res.meta?.total_count ?? res.data.length;
18
+ if (format === "json") {
19
+ return printJson({
20
+ ok: true,
21
+ base_url: auth.baseUrl,
22
+ api_key_source: auth.apiKeySource,
23
+ phone_numbers_total: total,
24
+ });
25
+ }
26
+ printLine(pc.green("Autenticado. Conta acessível."));
27
+ printLine(`URL base ${auth.baseUrl}`);
28
+ printLine(`Origem da chave ${auth.apiKeySource}`);
29
+ printLine(`Números ${total}`);
30
+ printLine(dim("Sonda: GET /phone_numbers?per_page=1"));
31
+ });
32
+ }
@@ -0,0 +1,87 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { context, toInt } from "./shared.js";
3
+ import { printJson, printTable, printDetail, printOffsetFooter, printLine, } from "../output.js";
4
+ /** Lê e parseia JSON de um arquivo. */
5
+ function readJsonFile(file) {
6
+ let raw;
7
+ try {
8
+ raw = readFileSync(file, "utf8");
9
+ }
10
+ catch {
11
+ throw new Error(`Não foi possível ler o arquivo: ${file}`);
12
+ }
13
+ try {
14
+ return JSON.parse(raw);
15
+ }
16
+ catch {
17
+ throw new Error(`Arquivo não é JSON válido: ${file}`);
18
+ }
19
+ }
20
+ export function registerTemplates(program) {
21
+ const templates = program
22
+ .command("templates")
23
+ .description("Gerenciar templates de mensagem");
24
+ templates
25
+ .command("list")
26
+ .description("Lista templates (paginação por offset)")
27
+ .option("--status <status>", "filtra por status")
28
+ .option("--category <categoria>", "filtra por categoria")
29
+ .option("--phone-number-id <id>", "filtra por número")
30
+ .option("--page <n>", "página")
31
+ .option("--per-page <n>", "itens por página")
32
+ .action(async (opts, cmd) => {
33
+ const { client, format } = context(cmd);
34
+ const res = await client.templates.list({
35
+ status: opts.status,
36
+ category: opts.category,
37
+ phone_number_id: opts.phoneNumberId,
38
+ page: toInt(opts.page),
39
+ per_page: toInt(opts.perPage),
40
+ });
41
+ if (format === "json")
42
+ return printJson(res);
43
+ printTable(res.data, [
44
+ { header: "ID", key: "id", max: 36 },
45
+ { header: "NOME", key: "name" },
46
+ { header: "IDIOMA", key: "language" },
47
+ { header: "CATEGORIA", key: "category" },
48
+ { header: "STATUS", key: "status" },
49
+ ]);
50
+ printOffsetFooter(res.meta);
51
+ });
52
+ templates
53
+ .command("get <id>")
54
+ .description("Detalha um template")
55
+ .action(async (id, _opts, cmd) => {
56
+ const { client, format } = context(cmd);
57
+ const data = await client.templates.get(id);
58
+ if (format === "json")
59
+ return printJson(data);
60
+ printDetail(data);
61
+ });
62
+ templates
63
+ .command("create")
64
+ .description("Cria um template (componentes via --components arquivo.json)")
65
+ .requiredOption("--name <nome>", "nome do template")
66
+ .requiredOption("--language <idioma>", "código de idioma (ex.: pt_BR)")
67
+ .requiredOption("--category <categoria>", "categoria (MARKETING, UTILITY, …)")
68
+ .requiredOption("--components <arquivo>", "arquivo JSON com o array de componentes")
69
+ .option("--waba-connection-id <id>", "conexão WABA alvo")
70
+ .option("--phone-number-id <id>", "número alvo")
71
+ .action(async (opts, cmd) => {
72
+ const { client, format } = context(cmd);
73
+ const components = readJsonFile(opts.components);
74
+ const data = await client.templates.create({
75
+ name: opts.name,
76
+ language: opts.language,
77
+ category: opts.category,
78
+ components: components,
79
+ waba_connection_id: opts.wabaConnectionId,
80
+ phone_number_id: opts.phoneNumberId,
81
+ });
82
+ if (format === "json")
83
+ return printJson(data);
84
+ printLine("Template criado.");
85
+ printDetail(data);
86
+ });
87
+ }
@@ -0,0 +1,28 @@
1
+ import { context, toInt } from "./shared.js";
2
+ import { printJson, printTable, printOffsetFooter } from "../output.js";
3
+ export function registerUsers(program) {
4
+ const users = program
5
+ .command("users")
6
+ .description("Usuários do workspace");
7
+ users
8
+ .command("list")
9
+ .description("Lista usuários (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.users.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: "EMAIL", key: "email" },
24
+ { header: "PAPEL", key: "role" },
25
+ ]);
26
+ printOffsetFooter(res.meta);
27
+ });
28
+ }
@@ -0,0 +1,116 @@
1
+ import { context, toInt, toBool } from "./shared.js";
2
+ import { printJson, printTable, printDetail, printCursorFooter, printLine, } from "../output.js";
3
+ export function registerWebhooks(program) {
4
+ const webhooks = program
5
+ .command("webhooks")
6
+ .description("Gerenciar endpoints de webhook");
7
+ webhooks
8
+ .command("list")
9
+ .description("Lista webhooks (paginação por cursor)")
10
+ .option("--limit <n>", "quantidade por página")
11
+ .option("--after <cursor>", "cursor da próxima página")
12
+ .option("--before <cursor>", "cursor da página anterior")
13
+ .action(async (opts, cmd) => {
14
+ const { client, format } = context(cmd);
15
+ const res = await client.webhooks.list({
16
+ limit: toInt(opts.limit),
17
+ after: opts.after,
18
+ before: opts.before,
19
+ });
20
+ if (format === "json")
21
+ return printJson(res);
22
+ printTable(res.data, [
23
+ { header: "ID", key: "id", max: 36 },
24
+ { header: "URL", key: "url", max: 50 },
25
+ { header: "ATIVO", key: "active" },
26
+ { header: "EVENTOS", key: "events" },
27
+ ]);
28
+ printCursorFooter(res.paging);
29
+ });
30
+ webhooks
31
+ .command("get <id>")
32
+ .description("Detalha um webhook")
33
+ .action(async (id, _opts, cmd) => {
34
+ const { client, format } = context(cmd);
35
+ const data = await client.webhooks.get(id);
36
+ if (format === "json")
37
+ return printJson(data);
38
+ printDetail(data);
39
+ });
40
+ webhooks
41
+ .command("create")
42
+ .description("Cria um webhook")
43
+ .requiredOption("--url <url>", "URL de entrega (obrigatório)")
44
+ .requiredOption("--events <lista>", "eventos separados por vírgula (obrigatório)")
45
+ .option("--secret <segredo>", "segredo para assinar as entregas")
46
+ .option("--active <bool>", "true | false")
47
+ .action(async (opts, cmd) => {
48
+ const { client, format } = context(cmd);
49
+ const events = String(opts.events)
50
+ .split(",")
51
+ .map((e) => e.trim())
52
+ .filter(Boolean);
53
+ const data = await client.webhooks.create({
54
+ url: opts.url,
55
+ events,
56
+ secret: opts.secret,
57
+ active: toBool(opts.active),
58
+ });
59
+ if (format === "json")
60
+ return printJson(data);
61
+ printLine("Webhook criado.");
62
+ printDetail(data);
63
+ });
64
+ webhooks
65
+ .command("update <id>")
66
+ .description("Atualiza um webhook")
67
+ .option("--url <url>", "nova URL")
68
+ .option("--events <lista>", "novos eventos (separados por vírgula)")
69
+ .option("--secret <segredo>", "novo segredo")
70
+ .option("--active <bool>", "true | false")
71
+ .action(async (id, opts, cmd) => {
72
+ const { client, format } = context(cmd);
73
+ const body = {};
74
+ if (opts.url !== undefined)
75
+ body.url = opts.url;
76
+ if (opts.events !== undefined) {
77
+ body.events = String(opts.events)
78
+ .split(",")
79
+ .map((e) => e.trim())
80
+ .filter(Boolean);
81
+ }
82
+ if (opts.secret !== undefined)
83
+ body.secret = opts.secret;
84
+ if (opts.active !== undefined)
85
+ body.active = toBool(opts.active);
86
+ if (Object.keys(body).length === 0) {
87
+ throw new Error("Informe ao menos um campo para atualizar.");
88
+ }
89
+ const data = await client.webhooks.update(id, body);
90
+ if (format === "json")
91
+ return printJson(data);
92
+ printLine("Webhook atualizado.");
93
+ printDetail(data);
94
+ });
95
+ webhooks
96
+ .command("delete <id>")
97
+ .description("Remove um webhook")
98
+ .action(async (id, _opts, cmd) => {
99
+ const { client, format } = context(cmd);
100
+ await client.webhooks.delete(id);
101
+ if (format === "json")
102
+ return printJson({ deleted: true });
103
+ printLine("Webhook removido.");
104
+ });
105
+ webhooks
106
+ .command("test <id>")
107
+ .description("Dispara uma entrega de teste para o webhook")
108
+ .action(async (id, _opts, cmd) => {
109
+ const { client, format } = context(cmd);
110
+ const result = await client.webhooks.test(id);
111
+ if (format === "json")
112
+ return printJson(result);
113
+ printLine("Entrega de teste disparada.");
114
+ printDetail(result);
115
+ });
116
+ }
package/dist/config.js ADDED
@@ -0,0 +1,77 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync, } from "node:fs";
4
+ /** URL base padrão da API pública do BotoZap. */
5
+ export const DEFAULT_BASE_URL = "https://botozap.com.br/api/v1";
6
+ /** Diretório de configuração: `~/.botozap/cli/`. */
7
+ export function configDir() {
8
+ return join(homedir(), ".botozap", "cli");
9
+ }
10
+ /** Caminho do arquivo de configuração: `~/.botozap/cli/config.json`. */
11
+ export function configPath() {
12
+ return join(configDir(), "config.json");
13
+ }
14
+ /** Lê o config do disco (ou objeto vazio se não existir/inválido). */
15
+ export function readConfig() {
16
+ const path = configPath();
17
+ if (!existsSync(path))
18
+ return {};
19
+ try {
20
+ const raw = readFileSync(path, "utf8");
21
+ const parsed = JSON.parse(raw);
22
+ return parsed && typeof parsed === "object" ? parsed : {};
23
+ }
24
+ catch {
25
+ return {};
26
+ }
27
+ }
28
+ /** Grava o config no disco, criando o diretório com permissão restrita. */
29
+ export function writeConfig(config) {
30
+ const dir = configDir();
31
+ const path = configPath();
32
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
33
+ // Permissões restritas ANTES de gravar o segredo: `mode` do writeFileSync só
34
+ // vale na criação; num arquivo preexistente frouxo (ex.: legado 0644), gravar
35
+ // primeiro abriria uma janela em que a API key fica legível por outros
36
+ // usuários locais (TOCTOU). Endurece → escreve → reforça.
37
+ chmodSync(dir, 0o700);
38
+ if (existsSync(path))
39
+ chmodSync(path, 0o600);
40
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", {
41
+ mode: 0o600,
42
+ });
43
+ chmodSync(path, 0o600);
44
+ }
45
+ /** Define uma chave individual no config persistido. */
46
+ export function setConfigValue(key, value) {
47
+ const current = readConfig();
48
+ current[key] = value;
49
+ writeConfig(current);
50
+ }
51
+ /**
52
+ * Resolve credenciais com a prioridade documentada:
53
+ * apiKey: --api-key > BOTOZAP_API_KEY > config.json
54
+ * baseUrl: --api-url > BOTOZAP_API_URL > config.json > DEFAULT_BASE_URL
55
+ */
56
+ export function resolveAuth(opts) {
57
+ const stored = readConfig();
58
+ let apiKey;
59
+ let apiKeySource = "none";
60
+ if (opts.apiKey) {
61
+ apiKey = opts.apiKey;
62
+ apiKeySource = "flag";
63
+ }
64
+ else if (process.env.BOTOZAP_API_KEY) {
65
+ apiKey = process.env.BOTOZAP_API_KEY;
66
+ apiKeySource = "env";
67
+ }
68
+ else if (stored.apiKey) {
69
+ apiKey = stored.apiKey;
70
+ apiKeySource = "config";
71
+ }
72
+ const baseUrl = opts.apiUrl ||
73
+ process.env.BOTOZAP_API_URL ||
74
+ stored.baseUrl ||
75
+ DEFAULT_BASE_URL;
76
+ return { apiKey, baseUrl: baseUrl.replace(/\/+$/, ""), apiKeySource };
77
+ }