@nodium.io/cli 0.1.0 → 0.2.1
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 +124 -19
- package/dist/args.js +25 -9
- package/dist/commands/account.js +142 -0
- package/dist/commands/conversations.js +92 -0
- package/dist/commands/flows.js +73 -0
- package/dist/commands/index.js +58 -0
- package/dist/commands/numbers.js +44 -0
- package/dist/commands/templates.js +157 -0
- package/dist/commands/tenants.js +100 -0
- package/dist/commands/webhooks.js +159 -0
- package/dist/config.js +48 -0
- package/dist/context.js +48 -0
- package/dist/index.js +42 -0
- package/dist/input.js +39 -0
- package/dist/listen.js +1 -1
- package/dist/output.js +38 -3
- package/dist/run.js +122 -105
- package/dist/send.js +66 -24
- package/package.json +4 -4
package/dist/output.js
CHANGED
|
@@ -4,6 +4,15 @@ const CODES = { dim: 2, red: 31, green: 32, yellow: 33, cyan: 36, bold: 1 };
|
|
|
4
4
|
export function paint(output, style, text) {
|
|
5
5
|
return output.color ? `\u001b[${CODES[style]}m${text}\u001b[0m` : text;
|
|
6
6
|
}
|
|
7
|
+
/** Le bloc « Next: » : les commandes à lancer ensuite, une par ligne. */
|
|
8
|
+
export function printNext(output, steps) {
|
|
9
|
+
if (!steps?.length)
|
|
10
|
+
return;
|
|
11
|
+
output.out('');
|
|
12
|
+
output.out('Next:');
|
|
13
|
+
for (const step of steps)
|
|
14
|
+
output.out(` ${step}`);
|
|
15
|
+
}
|
|
7
16
|
/** L'heure locale, `HH:MM:SS`. */
|
|
8
17
|
export function clock(date = new Date()) {
|
|
9
18
|
return date.toTimeString().slice(0, 8);
|
|
@@ -22,9 +31,7 @@ export function describeError(error, baseUrl) {
|
|
|
22
31
|
return error instanceof Error ? error.message : String(error);
|
|
23
32
|
const lines = [`${error.status ? `Error ${error.status}` : 'Error'} ${error.code}: ${error.message}`];
|
|
24
33
|
if (error.status === 401)
|
|
25
|
-
lines.push('The key is missing, malformed or revoked: check NODIUM_KEY
|
|
26
|
-
if (error.status === 403)
|
|
27
|
-
lines.push('This key does not have the role the command needs (`listen` reads the event log: `admin`).');
|
|
34
|
+
lines.push('The key is missing, malformed, expired or revoked: check --key, NODIUM_KEY, or run `nodium login` again.');
|
|
28
35
|
if (error.retryAfter !== null)
|
|
29
36
|
lines.push(`Retry in ${error.retryAfter} s.`);
|
|
30
37
|
if (error.requestId)
|
|
@@ -37,3 +44,31 @@ export function describeError(error, baseUrl) {
|
|
|
37
44
|
}
|
|
38
45
|
return lines.join('\n');
|
|
39
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Le refus en JSON, sur la sortie d'erreur : `{ "error": { status, code,
|
|
49
|
+
* message, requestId } }`. Une faute d'usage a `status` 0 et le code `usage_error`.
|
|
50
|
+
*/
|
|
51
|
+
export function errorJson(error) {
|
|
52
|
+
const body = error instanceof NodiumError
|
|
53
|
+
? { status: error.status, code: error.code, message: error.message, requestId: error.requestId }
|
|
54
|
+
: { status: 0, code: error instanceof UsageError ? 'usage_error' : 'cli_error', message: error instanceof Error ? error.message : String(error), requestId: null };
|
|
55
|
+
return JSON.stringify({ error: body }, null, 2);
|
|
56
|
+
}
|
|
57
|
+
/** Des colonnes alignées, une ligne par entrée ; la dernière colonne n'est pas complétée. */
|
|
58
|
+
export function table(rows) {
|
|
59
|
+
const widths = [];
|
|
60
|
+
for (const row of rows)
|
|
61
|
+
row.forEach((cell, i) => { widths[i] = Math.max(widths[i] ?? 0, cell.length); });
|
|
62
|
+
return rows.map(row => row.map((cell, i) => (i === row.length - 1 ? cell : cell.padEnd(widths[i]))).join(' ').trimEnd());
|
|
63
|
+
}
|
|
64
|
+
/** Une date ISO en `AAAA-MM-JJ HH:MM`, ou `—`. */
|
|
65
|
+
export function when(iso) {
|
|
66
|
+
return iso ? iso.slice(0, 16).replace('T', ' ') : '—';
|
|
67
|
+
}
|
|
68
|
+
/** Un objet en lignes `clé valeur` ; ce qui est imbriqué s'écrit en JSON compact. */
|
|
69
|
+
export function details(object) {
|
|
70
|
+
return table(Object.entries(object).map(([key, value]) => [
|
|
71
|
+
key,
|
|
72
|
+
value === null || value === undefined ? '—' : typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
73
|
+
]));
|
|
74
|
+
}
|
package/dist/run.js
CHANGED
|
@@ -2,71 +2,64 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { DEFAULT_BASE_URL, Nodium } from '@nodium.io/whatsapp';
|
|
4
4
|
import { UsageError, flag, hasFlag, parseArgs, rejectUnknown } from './args.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { COMMANDS, findCommands } from './commands/index.js';
|
|
6
|
+
import { configPath as defaultConfigPath, readConfig, resolveKey } from './config.js';
|
|
7
|
+
import { confirmWrite, outputMode } from './context.js';
|
|
8
|
+
import { pause } from './listen.js';
|
|
9
|
+
import { describeError, errorJson, paint, table, printNext } from './output.js';
|
|
8
10
|
/**
|
|
9
|
-
* La commande `nodium` : lit la ligne de commande,
|
|
10
|
-
*
|
|
11
|
+
* La commande `nodium` : lit la ligne de commande, cherche la commande dans la
|
|
12
|
+
* table (`commands/index.ts`), branche le client, rend le code de sortie
|
|
13
|
+
* (0 réussi, 1 refusé par Nodium ou par Meta, 2 mal appelé).
|
|
11
14
|
*
|
|
12
|
-
* ⚠️ La clé se donne de préférence par `
|
|
13
|
-
* l'historique du terminal.
|
|
15
|
+
* ⚠️ La clé se donne de préférence par `nodium login` ou `NODIUM_KEY` :
|
|
16
|
+
* `--key` reste dans l'historique du terminal.
|
|
14
17
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/** Les drapeaux qu'accepte toute commande. */
|
|
19
|
+
const COMMON = ['key', 'tenant', 'base-url', 'output', 'yes', 'no-color', 'help'];
|
|
20
|
+
const COMMON_HELP = `Options for every command
|
|
21
|
+
--key <nod_…> API key. Default: $NODIUM_KEY, then the key saved by \`nodium login\`.
|
|
22
|
+
--tenant <id> Act for one client: its id or your own externalId.
|
|
23
|
+
--output <json|human> json: the API's object, for scripts and agents. Default: human in a
|
|
24
|
+
terminal, json when piped.
|
|
25
|
+
-y, --yes Approve a command that sends or changes something, without asking.
|
|
26
|
+
Required when not in a terminal.
|
|
27
|
+
--base-url <url> API address. Default: ${DEFAULT_BASE_URL} ($NODIUM_BASE_URL).
|
|
28
|
+
--no-color No colors (also: $NO_COLOR).
|
|
29
|
+
-h, --help This help, or the help of a command.
|
|
30
|
+
-v, --version The version.`;
|
|
31
|
+
function usage(command) {
|
|
32
|
+
return ['nodium', ...command.path, ...(command.args ?? [])].join(' ');
|
|
33
|
+
}
|
|
34
|
+
export function globalHelp() {
|
|
35
|
+
return `nodium: the Nodium WhatsApp API from your terminal.
|
|
20
36
|
|
|
21
37
|
Commands
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
${table(COMMANDS.map(c => [` ${usage(c)}`, c.summary])).join('\n')}
|
|
39
|
+
|
|
40
|
+
${COMMON_HELP}
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
--tenant <id> Act for one client: its id or your own externalId.
|
|
28
|
-
--base-url <url> API address. Default: ${DEFAULT_BASE_URL} ($NODIUM_BASE_URL).
|
|
29
|
-
--no-color No colors (also: $NO_COLOR).
|
|
30
|
-
-h, --help This help, or the help of a command.
|
|
31
|
-
-v, --version The version.
|
|
42
|
+
Exit codes: 0 done, 1 refused by Nodium or Meta, 2 wrong command line.
|
|
43
|
+
More: nodium help <command>, for example \`nodium help templates wait\`.
|
|
32
44
|
`;
|
|
33
|
-
|
|
45
|
+
}
|
|
46
|
+
export function commandHelp(command) {
|
|
47
|
+
return `${usage(command)}: ${command.summary}
|
|
34
48
|
|
|
35
49
|
Usage
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Options
|
|
39
|
-
--forward <url> Your local address, for example http://localhost:3000/webhook. Required.
|
|
40
|
-
--secret <whsec> Signs each call with X-Nodium-Signature (t=…,v1=…), as in production.
|
|
41
|
-
Use the secret of your webhook subscription, or any string your code also
|
|
42
|
-
uses to verify: the signature is computed here. Default: $NODIUM_WEBHOOK_SECRET.
|
|
43
|
-
--events <a,b> Only these events (message.received,message.failed…). Default: all.
|
|
44
|
-
--from <seq> Start after this event number (the "Resume with" line printed on exit).
|
|
45
|
-
Default: from now.
|
|
46
|
-
--interval <ms> Wait between two reads when nothing is new. Default: ${DEFAULT_INTERVAL_MS}.
|
|
47
|
-
--verbose Also print each event's data.
|
|
48
|
-
|
|
49
|
-
The key needs the admin role: it reads the event log (GET /events). Events of
|
|
50
|
-
every client of the account arrive, unless --tenant names one or the key is
|
|
51
|
-
pinned to one. A call your server answers with a non-2xx status, or not at
|
|
52
|
-
all, is shown and not retried.
|
|
50
|
+
${usage(command)}${command.flags?.length ? ' [options]' : ''}
|
|
51
|
+
${command.help ? `\nOptions\n${command.help}\n` : ''}
|
|
52
|
+
Options for every command: \`nodium --help\`.
|
|
53
53
|
`;
|
|
54
|
-
|
|
54
|
+
}
|
|
55
|
+
function groupHelp(noun, commands) {
|
|
56
|
+
return `nodium ${noun}
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
nodium send --channel <id> --to <+33612345678> --text "Hello"
|
|
58
|
-
nodium send --channel <id> --to <+33612345678> --template order_ready --language en --var 1=Karim
|
|
58
|
+
${table(commands.map(c => [` ${usage(c)}`, c.summary])).join('\n')}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
--channel <id> The number to send from (GET /channels). Required.
|
|
62
|
-
--to <number> The recipient, in international format. Or --recipient wa:… for a
|
|
63
|
-
contact who shows no number.
|
|
64
|
-
--text <text> Free text: only within 24 hours of the person's last message.
|
|
65
|
-
--template <name> An approved template: the only way to write first.
|
|
66
|
-
--language <code> The template language, for example en or fr.
|
|
67
|
-
--var <n=value> A template variable (repeat it): --var 1=Karim --var 2=Friday.
|
|
68
|
-
--idempotency-key <k> Your own anti-duplicate key (8 to 200 characters).
|
|
60
|
+
More: nodium help ${noun} <${commands.map(c => c.path[1]).join('|')}>
|
|
69
61
|
`;
|
|
62
|
+
}
|
|
70
63
|
function packageVersion() {
|
|
71
64
|
try {
|
|
72
65
|
const file = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
@@ -76,74 +69,98 @@ function packageVersion() {
|
|
|
76
69
|
return 'unknown';
|
|
77
70
|
}
|
|
78
71
|
}
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
const NO_IO = {
|
|
73
|
+
ask: async () => '',
|
|
74
|
+
readSecret: async () => '',
|
|
75
|
+
readStdin: async () => ''
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* La commande nommée par les mots de la ligne : `templates wait` ou `send`.
|
|
79
|
+
* Rend aussi les mots qui restent (ses arguments).
|
|
80
|
+
*/
|
|
81
|
+
export function resolveCommand(words) {
|
|
82
|
+
const [noun, verb, ...more] = words;
|
|
83
|
+
if (!noun)
|
|
84
|
+
return { command: null, rest: [], group: [] };
|
|
85
|
+
const group = findCommands([noun]);
|
|
86
|
+
const exact = group.find(c => c.path.length === 1);
|
|
87
|
+
if (exact)
|
|
88
|
+
return { command: exact, rest: words.slice(1), group };
|
|
89
|
+
const command = verb ? group.find(c => c.path[1] === verb) ?? null : null;
|
|
90
|
+
return { command, rest: command ? more : [], group };
|
|
91
|
+
}
|
|
81
92
|
export async function run(argv, context) {
|
|
82
93
|
const { output, env } = context;
|
|
83
94
|
let baseUrl = env.NODIUM_BASE_URL || DEFAULT_BASE_URL;
|
|
95
|
+
let json = false;
|
|
84
96
|
try {
|
|
85
97
|
const args = parseArgs(argv);
|
|
98
|
+
json = outputMode(flag(args, 'output'), Boolean(context.stdoutIsTty)) === 'json';
|
|
86
99
|
if (hasFlag(args, 'version')) {
|
|
87
100
|
output.out(packageVersion());
|
|
88
101
|
return 0;
|
|
89
102
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
// `nodium help templates wait` vaut `nodium templates wait --help`.
|
|
104
|
+
const asksHelp = args.command === 'help' || hasFlag(args, 'help');
|
|
105
|
+
const words = args.command === 'help' ? args.positionals : [args.command ?? '', ...args.positionals].filter(Boolean);
|
|
106
|
+
if (!words.length) {
|
|
107
|
+
output.out(globalHelp());
|
|
108
|
+
return asksHelp ? 0 : 2;
|
|
93
109
|
}
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
const { command, rest, group } = resolveCommand(words);
|
|
111
|
+
if (!command) {
|
|
112
|
+
if (!group.length)
|
|
113
|
+
throw new UsageError(`Unknown command "${words[0]}". Try \`nodium --help\`.`);
|
|
114
|
+
if (asksHelp || words.length === 1) {
|
|
115
|
+
output.out(groupHelp(words[0], group));
|
|
116
|
+
return asksHelp ? 0 : 2;
|
|
117
|
+
}
|
|
118
|
+
throw new UsageError(`Unknown command "${words.slice(0, 2).join(' ')}". Try \`nodium help ${words[0]}\`.`);
|
|
96
119
|
}
|
|
97
|
-
if (
|
|
98
|
-
output.out(
|
|
120
|
+
if (asksHelp) {
|
|
121
|
+
output.out(commandHelp(command));
|
|
99
122
|
return 0;
|
|
100
123
|
}
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
const name = command.path.join(' ');
|
|
125
|
+
const commandArgs = { ...args, command: name, positionals: rest };
|
|
126
|
+
rejectUnknown(commandArgs, [...COMMON, ...(command.flags ?? [])], name, command.args?.length ?? 0);
|
|
127
|
+
const path = context.configPath ?? defaultConfigPath(env);
|
|
128
|
+
const config = readConfig(path);
|
|
129
|
+
baseUrl = flag(args, 'base-url') ?? (env.NODIUM_BASE_URL || config.baseUrl || DEFAULT_BASE_URL);
|
|
130
|
+
let client = null;
|
|
131
|
+
const nodium = () => {
|
|
132
|
+
if (!client) {
|
|
133
|
+
const { key } = resolveKey(flag(args, 'key'), env, config);
|
|
134
|
+
client = new Nodium(key, { baseUrl, ...(context.fetch ? { fetch: context.fetch } : {}) });
|
|
135
|
+
}
|
|
136
|
+
return client;
|
|
137
|
+
};
|
|
138
|
+
const io = context.io ?? NO_IO;
|
|
139
|
+
const interactive = Boolean(context.interactive);
|
|
140
|
+
const ctx = {
|
|
141
|
+
args: commandArgs, output, json, interactive, env, signal: context.signal, io,
|
|
142
|
+
...(context.fetch ? { fetch: context.fetch } : {}),
|
|
143
|
+
sleep: context.sleep ?? pause,
|
|
144
|
+
baseUrl, configPath: path, config,
|
|
145
|
+
nodium,
|
|
146
|
+
scope: () => {
|
|
147
|
+
const tenant = flag(args, 'tenant');
|
|
148
|
+
return tenant ? nodium().as(tenant) : nodium();
|
|
149
|
+
},
|
|
150
|
+
print: (data, human, next) => {
|
|
151
|
+
if (json)
|
|
152
|
+
return output.out(JSON.stringify(data, null, 2));
|
|
153
|
+
human();
|
|
154
|
+
printNext(output, next);
|
|
155
|
+
},
|
|
156
|
+
confirm: what => confirmWrite(what, { yes: hasFlag(args, 'yes'), interactive, ask: io.ask })
|
|
157
|
+
};
|
|
158
|
+
if (command.needsKey !== false)
|
|
159
|
+
nodium();
|
|
160
|
+
return await command.run(ctx);
|
|
111
161
|
}
|
|
112
162
|
catch (error) {
|
|
113
|
-
output.err(paint(output, 'red', describeError(error, baseUrl)));
|
|
163
|
+
output.err(json ? errorJson(error) : paint(output, 'red', describeError(error, baseUrl)));
|
|
114
164
|
return error instanceof UsageError ? 2 : 1;
|
|
115
165
|
}
|
|
116
166
|
}
|
|
117
|
-
async function runListen(args, scope, baseUrl, env, context) {
|
|
118
|
-
rejectUnknown(args, [...COMMON, 'forward', 'secret', 'events', 'from', 'interval', 'verbose']);
|
|
119
|
-
const forward = parseForwardUrl(flag(args, 'forward'));
|
|
120
|
-
const events = parseEventFilter(flag(args, 'events'));
|
|
121
|
-
const from = readInteger(args, 'from', 0);
|
|
122
|
-
const interval = readInteger(args, 'interval', 1);
|
|
123
|
-
const summary = await listen({
|
|
124
|
-
scope, baseUrl, forward, events,
|
|
125
|
-
secret: flag(args, 'secret') ?? env.NODIUM_WEBHOOK_SECRET ?? null,
|
|
126
|
-
from,
|
|
127
|
-
intervalMs: interval ?? DEFAULT_INTERVAL_MS,
|
|
128
|
-
verbose: hasFlag(args, 'verbose'),
|
|
129
|
-
signal: context.signal,
|
|
130
|
-
output: context.output,
|
|
131
|
-
...(context.fetch ? { fetch: context.fetch } : {})
|
|
132
|
-
});
|
|
133
|
-
context.output.out(`Stopped. ${summary.forwarded} forwarded, ${summary.failed} not answered with a 2xx. Resume with --from ${summary.after}.`);
|
|
134
|
-
return 0;
|
|
135
|
-
}
|
|
136
|
-
async function runSend(args, scope, output) {
|
|
137
|
-
rejectUnknown(args, [...COMMON, 'channel', 'to', 'recipient', 'text', 'template', 'language', 'var', 'idempotency-key']);
|
|
138
|
-
return await send(scope, readSendInput(args), output);
|
|
139
|
-
}
|
|
140
|
-
/** Un entier au moins égal à `min`, ou `null` si le drapeau est absent. */
|
|
141
|
-
function readInteger(args, name, min) {
|
|
142
|
-
const text = flag(args, name);
|
|
143
|
-
if (text === undefined)
|
|
144
|
-
return null;
|
|
145
|
-
const value = Number(text);
|
|
146
|
-
if (!Number.isInteger(value) || value < min)
|
|
147
|
-
throw new UsageError(`--${name} must be a whole number, ${min} or more.`);
|
|
148
|
-
return value;
|
|
149
|
-
}
|
package/dist/send.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { UsageError, flag, flagAll } from './args.js';
|
|
1
|
+
import { UsageError, flag, flagAll, hasFlag } from './args.js';
|
|
2
|
+
import { readJsonInput } from './input.js';
|
|
2
3
|
import { paint } from './output.js';
|
|
3
4
|
/**
|
|
4
|
-
* `nodium send` : un message
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* `nodium send` : un message. Du texte libre (dans les 24 h qui suivent le
|
|
6
|
+
* dernier message de la personne) ou un modèle approuvé (le seul envoi
|
|
7
|
+
* possible pour écrire en premier) par drapeaux ; tout autre message (fichier,
|
|
8
|
+
* boutons, liste…) en JSON, par `--input` ou `--stdin`.
|
|
7
9
|
*
|
|
8
10
|
* ⚠️ Un envoi refusé par Meta répond HTTP 200 : la commande lit
|
|
9
11
|
* `message.status` et sort en erreur (code 1) quand il vaut `failed`.
|
|
@@ -44,27 +46,67 @@ export function readSendInput(args) {
|
|
|
44
46
|
...(Object.keys(variables).length ? { variables } : {})
|
|
45
47
|
};
|
|
46
48
|
}
|
|
49
|
+
/** Les drapeaux qui décrivent le message ; avec `--input` ou `--stdin`, aucun ne se donne. */
|
|
50
|
+
const MESSAGE_FLAGS = ['channel', 'to', 'recipient', 'text', 'template', 'language', 'var', 'idempotency-key'];
|
|
51
|
+
/** Le message à envoyer : le JSON de `--input`/`--stdin` (un `SendInput`), sinon les drapeaux. */
|
|
52
|
+
async function readMessage(ctx) {
|
|
53
|
+
const json = await readJsonInput(ctx);
|
|
54
|
+
if (!json)
|
|
55
|
+
return readSendInput(ctx.args);
|
|
56
|
+
const mixed = MESSAGE_FLAGS.find(name => hasFlag(ctx.args, name));
|
|
57
|
+
if (mixed)
|
|
58
|
+
throw new UsageError(`--${mixed} cannot be mixed with --input or --stdin: put everything in the JSON.`);
|
|
59
|
+
if (typeof json.channelId !== 'string' || (typeof json.to !== 'string' && typeof json.recipient !== 'string')) {
|
|
60
|
+
throw new UsageError('The JSON needs "channelId" and "to" (or "recipient"), plus "type" and its fields.');
|
|
61
|
+
}
|
|
62
|
+
return json;
|
|
63
|
+
}
|
|
47
64
|
/** Envoie, écrit le résultat, rend le code de sortie. */
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const message = result.message;
|
|
65
|
+
async function send(ctx) {
|
|
66
|
+
const input = await readMessage(ctx);
|
|
51
67
|
const target = input.to ?? input.recipient;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (message
|
|
59
|
-
output.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
await ctx.confirm(`Send a ${input.type ?? 'message'} to ${target} from number ${input.channelId}`);
|
|
69
|
+
const result = await ctx.scope().messages.send(input);
|
|
70
|
+
const message = result.message;
|
|
71
|
+
const failed = message?.status === 'failed';
|
|
72
|
+
ctx.print(result, () => {
|
|
73
|
+
const { output } = ctx;
|
|
74
|
+
if (!message)
|
|
75
|
+
return output.out(`Sent to ${target}.`);
|
|
76
|
+
if (failed) {
|
|
77
|
+
output.err(paint(output, 'red', `Message ${message.id} to ${target} failed.`));
|
|
78
|
+
if (message.errorInfo) {
|
|
79
|
+
output.err(`${message.errorInfo.title}`);
|
|
80
|
+
output.err(`What to do (${message.errorInfo.actor}): ${message.errorInfo.action}`);
|
|
81
|
+
}
|
|
82
|
+
else if (message.error) {
|
|
83
|
+
output.err(message.error);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
64
86
|
}
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
output.out(paint(output, 'dim', `Follow it: GET /messages/${message.id}, or the message.delivered and message.read events.`));
|
|
69
|
-
return 0;
|
|
87
|
+
output.out(`${paint(output, 'green', message.status)} message ${message.id} to ${target}${result.duplicate ? ' (already sent with this idempotency key: nothing left again)' : ''}`);
|
|
88
|
+
}, message ? [`nodium messages get ${message.id}`] : []);
|
|
89
|
+
return failed ? 1 : 0;
|
|
70
90
|
}
|
|
91
|
+
export const SEND_COMMAND = {
|
|
92
|
+
path: ['send'], summary: 'Send a WhatsApp message. Asks first.',
|
|
93
|
+
flags: [...MESSAGE_FLAGS, 'input', 'stdin'],
|
|
94
|
+
help: ` --channel <id> The number to send from (\`nodium numbers list\`). Required.
|
|
95
|
+
--to <number> The recipient, in international format. Or --recipient wa:… for a
|
|
96
|
+
contact who shows no number.
|
|
97
|
+
--text <text> Free text: only within 24 hours of the person's last message.
|
|
98
|
+
--template <name> An approved template: the only way to write first.
|
|
99
|
+
--language <code> The template language, for example en or fr.
|
|
100
|
+
--var <n=value> A template variable (repeat it): --var 1=Karim --var 2=Friday.
|
|
101
|
+
--idempotency-key <k> Your own anti-duplicate key (8 to 200 characters).
|
|
102
|
+
--input <file.json> Any message as JSON (image, buttons, list, location…), or --stdin.
|
|
103
|
+
The fields of POST /messages: channelId, to, type and its fields.
|
|
104
|
+
|
|
105
|
+
Examples
|
|
106
|
+
nodium send --channel <id> --to +33612345678 --text "Hello"
|
|
107
|
+
nodium send --channel <id> --to +33612345678 --template order_ready --language en --var 1=Karim
|
|
108
|
+
echo '{"channelId":"<id>","to":"+33612345678","type":"image","mediaId":"<id>"}' | nodium send --stdin --yes
|
|
109
|
+
|
|
110
|
+
A send Meta refuses answers HTTP 200: the command reads the message status and exits 1.`,
|
|
111
|
+
run: send
|
|
112
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodium.io/cli",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "The nodium command:
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "The nodium command: the Nodium WhatsApp API from your terminal — numbers, conversations, templates, webhooks, clients, Flows — and your webhooks replayed to your local server, signed like production.",
|
|
5
5
|
"keywords": ["nodium", "cli", "whatsapp", "whatsapp-api", "whatsapp-cloud-api", "webhooks", "local-development"],
|
|
6
6
|
"homepage": "https://nodium.io/en/docs/api",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "By-Tech.io",
|
|
9
9
|
"type": "module",
|
|
10
|
-
"engines": { "node": ">=
|
|
10
|
+
"engines": { "node": ">=20" },
|
|
11
11
|
"bin": { "nodium": "./dist/index.js" },
|
|
12
12
|
"files": ["dist", "README.md", "LICENSE"],
|
|
13
13
|
"publishConfig": { "access": "public" },
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"prepublishOnly": "npm run build"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@nodium.io/whatsapp": "^0.1
|
|
20
|
+
"@nodium.io/whatsapp": "^0.3.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^26.4.0",
|