@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 +21 -0
- package/README.md +215 -0
- package/dist/client.js +24 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/contacts.js +110 -0
- package/dist/commands/conversations.js +60 -0
- package/dist/commands/customers.js +86 -0
- package/dist/commands/deliveries.js +40 -0
- package/dist/commands/login.js +61 -0
- package/dist/commands/logs.js +38 -0
- package/dist/commands/media.js +28 -0
- package/dist/commands/messages.js +113 -0
- package/dist/commands/numbers.js +51 -0
- package/dist/commands/setup-links.js +63 -0
- package/dist/commands/shared.js +37 -0
- package/dist/commands/status.js +32 -0
- package/dist/commands/templates.js +87 -0
- package/dist/commands/users.js +28 -0
- package/dist/commands/webhooks.js +116 -0
- package/dist/config.js +77 -0
- package/dist/index.js +74 -0
- package/dist/output.js +101 -0
- package/dist/render-error.js +100 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { resolveFormat } from "./output.js";
|
|
6
|
+
import { renderErrorText, renderErrorJson } from "./render-error.js";
|
|
7
|
+
import { registerMessages } from "./commands/messages.js";
|
|
8
|
+
import { registerConversations } from "./commands/conversations.js";
|
|
9
|
+
import { registerContacts } from "./commands/contacts.js";
|
|
10
|
+
import { registerMedia } from "./commands/media.js";
|
|
11
|
+
import { registerCustomers } from "./commands/customers.js";
|
|
12
|
+
import { registerSetupLinks } from "./commands/setup-links.js";
|
|
13
|
+
import { registerNumbers } from "./commands/numbers.js";
|
|
14
|
+
import { registerTemplates } from "./commands/templates.js";
|
|
15
|
+
import { registerWebhooks } from "./commands/webhooks.js";
|
|
16
|
+
import { registerDeliveries } from "./commands/deliveries.js";
|
|
17
|
+
import { registerLogs } from "./commands/logs.js";
|
|
18
|
+
import { registerUsers } from "./commands/users.js";
|
|
19
|
+
import { registerConfig } from "./commands/config.js";
|
|
20
|
+
import { registerStatus } from "./commands/status.js";
|
|
21
|
+
import { registerLogin } from "./commands/login.js";
|
|
22
|
+
const { version: VERSION } = createRequire(import.meta.url)("../package.json");
|
|
23
|
+
const program = new Command();
|
|
24
|
+
program
|
|
25
|
+
.name("botozap")
|
|
26
|
+
.description("CLI dev-first do BotoZap — WhatsApp Cloud API multi-tenant (a Kapso brasileira).")
|
|
27
|
+
.version(VERSION, "-v, --version", "mostra a versão")
|
|
28
|
+
.option("--api-key <chave>", "API key (sobrepõe env e config; prefira BOTOZAP_API_KEY ou `botozap login` — argv fica no histórico do shell)")
|
|
29
|
+
.option("--api-url <url>", "URL base da API (sobrepõe env e config)")
|
|
30
|
+
.option("-o, --output <formato>", "formato de saída: human | json", "human")
|
|
31
|
+
.showHelpAfterError("(use --help para ver as opções)");
|
|
32
|
+
// Captura o formato de saída resolvido (flag global -o/--output) antes de cada
|
|
33
|
+
// ação, para o handler central de erro renderizar human vs. json de forma
|
|
34
|
+
// consistente com o que o comando usaria. `preAction` só roda para comandos com
|
|
35
|
+
// ação (não para --help/--version), então não altera esses caminhos.
|
|
36
|
+
let selectedFormat = "human";
|
|
37
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
38
|
+
selectedFormat = resolveFormat(actionCommand.optsWithGlobals().output);
|
|
39
|
+
});
|
|
40
|
+
// Recursos
|
|
41
|
+
registerMessages(program);
|
|
42
|
+
registerConversations(program);
|
|
43
|
+
registerContacts(program);
|
|
44
|
+
registerMedia(program);
|
|
45
|
+
registerCustomers(program);
|
|
46
|
+
registerSetupLinks(program);
|
|
47
|
+
registerNumbers(program);
|
|
48
|
+
registerTemplates(program);
|
|
49
|
+
registerWebhooks(program);
|
|
50
|
+
registerDeliveries(program);
|
|
51
|
+
registerLogs(program);
|
|
52
|
+
registerUsers(program);
|
|
53
|
+
// Sessão / utilidades
|
|
54
|
+
registerLogin(program);
|
|
55
|
+
registerConfig(program);
|
|
56
|
+
registerStatus(program);
|
|
57
|
+
async function main() {
|
|
58
|
+
try {
|
|
59
|
+
await program.parseAsync(process.argv);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
// Erros (incl. o objeto json estruturado) vão para stderr — stdout fica
|
|
63
|
+
// reservado à saída de sucesso, para não poluir um `-o json` consumido por
|
|
64
|
+
// script. Exit code uniforme = 1 é decisão de design.
|
|
65
|
+
if (selectedFormat === "json") {
|
|
66
|
+
process.stderr.write(JSON.stringify(renderErrorJson(err), null, 2) + "\n");
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
process.stderr.write(pc.red(renderErrorText(err)) + "\n");
|
|
70
|
+
}
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
void main();
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
/** Resolve o formato a partir do valor da flag `-o/--output`. */
|
|
3
|
+
export function resolveFormat(value) {
|
|
4
|
+
return value === "json" ? "json" : "human";
|
|
5
|
+
}
|
|
6
|
+
/** Imprime JSON cru (indentado) — modo `--output json` para scripting. */
|
|
7
|
+
export function printJson(value) {
|
|
8
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
9
|
+
}
|
|
10
|
+
/** Mensagem informativa simples (stderr não; vai pro stdout). */
|
|
11
|
+
export function printLine(line) {
|
|
12
|
+
process.stdout.write(line + "\n");
|
|
13
|
+
}
|
|
14
|
+
export function dim(text) {
|
|
15
|
+
return pc.dim(text);
|
|
16
|
+
}
|
|
17
|
+
function valueToString(value) {
|
|
18
|
+
if (value === null || value === undefined)
|
|
19
|
+
return "";
|
|
20
|
+
if (typeof value === "object")
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
24
|
+
function truncate(text, max = 48) {
|
|
25
|
+
const flat = text.replace(/\s+/g, " ");
|
|
26
|
+
return flat.length > max ? flat.slice(0, max - 1) + "…" : flat;
|
|
27
|
+
}
|
|
28
|
+
function pick(row, key) {
|
|
29
|
+
if (!key.includes("."))
|
|
30
|
+
return row[key];
|
|
31
|
+
return key.split(".").reduce((acc, part) => {
|
|
32
|
+
if (acc && typeof acc === "object") {
|
|
33
|
+
return acc[part];
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}, row);
|
|
37
|
+
}
|
|
38
|
+
/** Tabela compacta de largura fixa para o modo `human`. */
|
|
39
|
+
export function printTable(rows, columns) {
|
|
40
|
+
if (rows.length === 0) {
|
|
41
|
+
printLine(dim("(nenhum resultado)"));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const cells = rows.map((row) => columns.map((col) => {
|
|
45
|
+
const raw = pick(row, col.key);
|
|
46
|
+
const str = col.format ? col.format(raw, row) : valueToString(raw);
|
|
47
|
+
return truncate(str, col.max ?? 48);
|
|
48
|
+
}));
|
|
49
|
+
const widths = columns.map((col, i) => {
|
|
50
|
+
const headerLen = col.header.length;
|
|
51
|
+
const maxCell = cells.reduce((w, r) => Math.max(w, (r[i] ?? "").length), 0);
|
|
52
|
+
return Math.max(headerLen, maxCell);
|
|
53
|
+
});
|
|
54
|
+
const headerLine = columns
|
|
55
|
+
.map((col, i) => pc.bold(col.header.padEnd(widths[i] ?? 0)))
|
|
56
|
+
.join(" ");
|
|
57
|
+
printLine(headerLine);
|
|
58
|
+
for (const row of cells) {
|
|
59
|
+
printLine(row.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" "));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Pares chave/valor para detalhe de um único recurso (modo `human`). */
|
|
63
|
+
export function printDetail(obj, keys) {
|
|
64
|
+
const entries = keys
|
|
65
|
+
? keys.map((k) => [k, pick(obj, k)])
|
|
66
|
+
: Object.entries(obj);
|
|
67
|
+
const width = Math.max(...entries.map(([k]) => k.length));
|
|
68
|
+
for (const [key, value] of entries) {
|
|
69
|
+
if (value === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
printLine(`${pc.bold(key.padEnd(width))} ${valueToString(value)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Rodapé de paginação por cursor. */
|
|
75
|
+
export function printCursorFooter(paging) {
|
|
76
|
+
if (!paging)
|
|
77
|
+
return;
|
|
78
|
+
const after = paging.cursors?.after;
|
|
79
|
+
const before = paging.cursors?.before;
|
|
80
|
+
const parts = [];
|
|
81
|
+
if (after)
|
|
82
|
+
parts.push(`próxima: --after ${after}`);
|
|
83
|
+
if (before)
|
|
84
|
+
parts.push(`anterior: --before ${before}`);
|
|
85
|
+
if (parts.length)
|
|
86
|
+
printLine(dim(parts.join(" ")));
|
|
87
|
+
}
|
|
88
|
+
/** Rodapé de paginação por offset. */
|
|
89
|
+
export function printOffsetFooter(meta) {
|
|
90
|
+
if (!meta)
|
|
91
|
+
return;
|
|
92
|
+
const { page, total_pages, total_count } = meta;
|
|
93
|
+
const bits = [];
|
|
94
|
+
if (page !== undefined && total_pages !== undefined) {
|
|
95
|
+
bits.push(`página ${page}/${total_pages}`);
|
|
96
|
+
}
|
|
97
|
+
if (total_count !== undefined)
|
|
98
|
+
bits.push(`${total_count} no total`);
|
|
99
|
+
if (bits.length)
|
|
100
|
+
printLine(dim(bits.join(" ")));
|
|
101
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { BotoZapError, ConfigError } from "./client.js";
|
|
2
|
+
function lowerKeys(h) {
|
|
3
|
+
const out = {};
|
|
4
|
+
for (const [k, v] of Object.entries(h))
|
|
5
|
+
out[k.toLowerCase()] = v;
|
|
6
|
+
return out;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Extrai `Retry-After`/`X-RateLimit-*` de um `BotoZapError` 429. Só nesse caso —
|
|
10
|
+
* fora dele os headers não têm significado de rate-limit. Vazio se ausentes.
|
|
11
|
+
* Lookup case-insensitive (o SDK guarda em minúsculas, mas não dependemos disso).
|
|
12
|
+
*/
|
|
13
|
+
export function rateLimitInfo(err) {
|
|
14
|
+
if (err.status !== 429 || !err.headers)
|
|
15
|
+
return {};
|
|
16
|
+
const h = lowerKeys(err.headers);
|
|
17
|
+
const info = {};
|
|
18
|
+
if (h["retry-after"] !== undefined)
|
|
19
|
+
info.retryAfter = h["retry-after"];
|
|
20
|
+
if (h["x-ratelimit-limit"] !== undefined)
|
|
21
|
+
info.limit = h["x-ratelimit-limit"];
|
|
22
|
+
if (h["x-ratelimit-remaining"] !== undefined) {
|
|
23
|
+
info.remaining = h["x-ratelimit-remaining"];
|
|
24
|
+
}
|
|
25
|
+
if (h["x-ratelimit-reset"] !== undefined)
|
|
26
|
+
info.reset = h["x-ratelimit-reset"];
|
|
27
|
+
return info;
|
|
28
|
+
}
|
|
29
|
+
function hasAny(info) {
|
|
30
|
+
return (info.retryAfter !== undefined ||
|
|
31
|
+
info.limit !== undefined ||
|
|
32
|
+
info.remaining !== undefined ||
|
|
33
|
+
info.reset !== undefined);
|
|
34
|
+
}
|
|
35
|
+
/** Sufixo humano com os dados do 429 (só o que veio nos headers). */
|
|
36
|
+
function rateLimitSuffix(info) {
|
|
37
|
+
const parts = [];
|
|
38
|
+
// Mostra o valor CRU de Retry-After (a spec HTTP permite segundos OU data;
|
|
39
|
+
// não afirmamos a unidade). Os contadores são informativos.
|
|
40
|
+
if (info.retryAfter !== undefined)
|
|
41
|
+
parts.push(`Retry-After: ${info.retryAfter}`);
|
|
42
|
+
if (info.remaining !== undefined)
|
|
43
|
+
parts.push(`restantes: ${info.remaining}`);
|
|
44
|
+
if (info.limit !== undefined)
|
|
45
|
+
parts.push(`limite: ${info.limit}`);
|
|
46
|
+
if (info.reset !== undefined)
|
|
47
|
+
parts.push(`reset: ${info.reset}`);
|
|
48
|
+
return parts.length ? ` — ${parts.join(", ")}` : "";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Texto humano de um erro. Base preservada:
|
|
52
|
+
* - `BotoZapError` → `Erro [code]: message`
|
|
53
|
+
* - `ConfigError`/genérico → `Erro: message`
|
|
54
|
+
* Num 429, ACRESCENTA (não reformata) o sufixo de rate-limit ao fim.
|
|
55
|
+
*/
|
|
56
|
+
export function renderErrorText(err) {
|
|
57
|
+
if (err instanceof BotoZapError) {
|
|
58
|
+
const suffix = err.status === 429 ? rateLimitSuffix(rateLimitInfo(err)) : "";
|
|
59
|
+
return `Erro [${err.code}]: ${err.message}${suffix}`;
|
|
60
|
+
}
|
|
61
|
+
if (err instanceof ConfigError)
|
|
62
|
+
return `Erro: ${err.message}`;
|
|
63
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
+
return `Erro: ${message}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Objeto de erro estruturado para `-o json` (envelope `{ error: {...} }`, o
|
|
68
|
+
* mesmo shape da API). Num 429 inclui `rate_limit` com os campos presentes.
|
|
69
|
+
*/
|
|
70
|
+
export function renderErrorJson(err) {
|
|
71
|
+
if (err instanceof BotoZapError) {
|
|
72
|
+
const info = rateLimitInfo(err);
|
|
73
|
+
return {
|
|
74
|
+
error: {
|
|
75
|
+
code: err.code,
|
|
76
|
+
message: err.message,
|
|
77
|
+
status: err.status,
|
|
78
|
+
...(hasAny(info)
|
|
79
|
+
? {
|
|
80
|
+
rate_limit: {
|
|
81
|
+
...(info.retryAfter !== undefined
|
|
82
|
+
? { retry_after: info.retryAfter }
|
|
83
|
+
: {}),
|
|
84
|
+
...(info.limit !== undefined ? { limit: info.limit } : {}),
|
|
85
|
+
...(info.remaining !== undefined
|
|
86
|
+
? { remaining: info.remaining }
|
|
87
|
+
: {}),
|
|
88
|
+
...(info.reset !== undefined ? { reset: info.reset } : {}),
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
: {}),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (err instanceof ConfigError) {
|
|
96
|
+
return { error: { code: "config_error", message: err.message } };
|
|
97
|
+
}
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
return { error: { code: "error", message } };
|
|
100
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@botozap/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI dev-first para a API pública do BotoZap (WhatsApp Cloud API multi-tenant).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"homepage": "https://botozap.com.br",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/bytecraft-fernando/botozap-js.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/bytecraft-fernando/botozap-js/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"botozap": "dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20.19"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^12.1.0",
|
|
32
|
+
"picocolors": "^1.1.1",
|
|
33
|
+
"@botozap/sdk": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^20.19.0",
|
|
37
|
+
"typescript": "^5.6.0",
|
|
38
|
+
"vitest": "^3.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
43
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"start": "node dist/index.js"
|
|
46
|
+
}
|
|
47
|
+
}
|