@nodium.io/cli 0.2.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 +2 -1
- package/dist/commands/account.js +49 -36
- package/dist/commands/conversations.js +5 -5
- package/dist/commands/flows.js +4 -2
- package/dist/commands/numbers.js +6 -4
- package/dist/commands/templates.js +18 -12
- package/dist/commands/tenants.js +7 -6
- package/dist/commands/webhooks.js +10 -11
- package/dist/output.js +9 -0
- package/dist/run.js +7 -2
- package/dist/send.js +1 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -134,7 +134,8 @@ Listening from now → http://localhost:3000/webhook
|
|
|
134
134
|
|
|
135
135
|
## Output and exit codes
|
|
136
136
|
|
|
137
|
-
- `--output human` (the default in a terminal): short lines and tables
|
|
137
|
+
- `--output human` (the default in a terminal): short lines and tables, then a
|
|
138
|
+
`Next:` block with the commands that usually come next.
|
|
138
139
|
- `--output json` (the default when the output is piped): the object the SDK
|
|
139
140
|
returns, unchanged, on stdout. A refusal goes to stderr as
|
|
140
141
|
`{ "error": { "status", "code", "message", "requestId" } }`.
|
package/dist/commands/account.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Nodium } from '@nodium.io/whatsapp';
|
|
1
|
+
import { Nodium, NodiumError } from '@nodium.io/whatsapp';
|
|
2
2
|
import { UsageError, flag, readInteger } from '../args.js';
|
|
3
|
-
import { deleteConfig, maskKey, writeConfig } from '../config.js';
|
|
3
|
+
import { deleteConfig, maskKey, resolveKey, writeConfig } from '../config.js';
|
|
4
|
+
import { take } from '../context.js';
|
|
4
5
|
import { paint, table, when } from '../output.js';
|
|
5
6
|
/**
|
|
6
7
|
* La clé et le compte : `login`, `logout`, `status`, `health`, `events`.
|
|
@@ -13,16 +14,17 @@ export function numberState(channel) {
|
|
|
13
14
|
return 'payment missing';
|
|
14
15
|
return channel.qualityRating ? `on (${channel.qualityRating.toLowerCase()})` : 'on';
|
|
15
16
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Les commandes à lancer ensuite, d'après les numéros et les abonnements.
|
|
19
|
+
* `activeWebhooks` à `null` : la clé ne lit pas les abonnements, rien n'est
|
|
20
|
+
* conseillé à leur sujet.
|
|
21
|
+
*/
|
|
22
|
+
export function nextSteps(numbers, activeWebhooks, seesAllClients) {
|
|
19
23
|
if (!numbers)
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
steps.push('All set: try `nodium send --channel <id> --to <+33…> --text "Hello"`.');
|
|
25
|
-
return steps;
|
|
24
|
+
return seesAllClients ? ['nodium tenants list', 'nodium connect-link new --tenant <id>'] : ['nodium connect-link new'];
|
|
25
|
+
if (activeWebhooks === 0)
|
|
26
|
+
return ['nodium listen --forward <local url>', 'nodium webhooks new --url <https://…> --event message.received'];
|
|
27
|
+
return ['nodium numbers list', 'nodium send --channel <id> --to <+33…> --text "Hello"'];
|
|
26
28
|
}
|
|
27
29
|
async function login(ctx) {
|
|
28
30
|
let key = flag(ctx.args, 'key');
|
|
@@ -40,7 +42,7 @@ async function login(ctx) {
|
|
|
40
42
|
ctx.print({ key: current, saved: ctx.configPath }, () => {
|
|
41
43
|
ctx.output.out(`${paint(ctx.output, 'green', 'Logged in')} with ${maskKey(key)} (account ${current.accountId}${current.pinned ? ', pinned to one client' : ''}).`);
|
|
42
44
|
ctx.output.out(paint(ctx.output, 'dim', `Saved to ${ctx.configPath} (readable by you only). NODIUM_KEY and --key still win over it.`));
|
|
43
|
-
});
|
|
45
|
+
}, ['nodium status']);
|
|
44
46
|
return 0;
|
|
45
47
|
}
|
|
46
48
|
async function logout(ctx) {
|
|
@@ -52,32 +54,45 @@ async function logout(ctx) {
|
|
|
52
54
|
});
|
|
53
55
|
return 0;
|
|
54
56
|
}
|
|
57
|
+
/** Au-delà, `status` écrit « 1000+ » : compter tous les clients d'un gros compte serait long. */
|
|
58
|
+
const CLIENTS_COUNTED = 1000;
|
|
55
59
|
async function status(ctx) {
|
|
56
60
|
const nodium = ctx.nodium();
|
|
57
61
|
const key = await nodium.keys.current();
|
|
58
62
|
const tenant = flag(ctx.args, 'tenant');
|
|
63
|
+
const seesAll = !tenant && !key.pinned;
|
|
59
64
|
// Une clé du compte, sans client nommé, voit les numéros de tous ses clients.
|
|
60
|
-
const numbers =
|
|
61
|
-
?
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const [numbers, clients, subscriptions] = await Promise.all([
|
|
66
|
+
seesAll ? nodium.channels.list({ scope: 'account' }) : ctx.scope().channels.list(),
|
|
67
|
+
// « Mon entreprise » n'est pas dans la liste des clients.
|
|
68
|
+
seesAll ? take(nodium.tenants.each({ limit: 100 }), CLIENTS_COUNTED).then(items => items.length) : Promise.resolve(null),
|
|
69
|
+
// Une clé épinglée à « Mon entreprise » ne lit pas ses abonnements (404) :
|
|
70
|
+
// ils portent les événements de tous les clients.
|
|
71
|
+
ctx.scope().webhooks.list().then(r => r.subscriptions, (error) => {
|
|
72
|
+
if (error instanceof NodiumError && error.status === 404)
|
|
73
|
+
return null;
|
|
74
|
+
throw error;
|
|
75
|
+
})
|
|
76
|
+
]);
|
|
77
|
+
const connected = numbers.filter(n => !n.isTest);
|
|
78
|
+
const active = subscriptions ? subscriptions.filter(s => s.isActive).length : null;
|
|
79
|
+
const next = nextSteps(connected.length, active, seesAll);
|
|
80
|
+
const webhooks = subscriptions ? { total: subscriptions.length, active } : null;
|
|
81
|
+
ctx.print({ key, clients, numbers, webhooks, next }, () => {
|
|
67
82
|
const { out } = ctx.output;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
out(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
83
|
+
const label = key.label ? ` "${key.label}"` : '';
|
|
84
|
+
const scope = key.client
|
|
85
|
+
? `pinned to ${key.client.isHome ? 'My company' : `"${key.client.name}"`} (${key.client.id})`
|
|
86
|
+
: 'every client of the account';
|
|
87
|
+
out(`Authentication: ${paint(ctx.output, 'green', 'authenticated')}`);
|
|
88
|
+
out(`Key: ${maskKey(resolveKey(flag(ctx.args, 'key'), ctx.env, ctx.config).key)}${label}, ${key.expiresAt ? `expires ${when(key.expiresAt)}` : 'never expires'}`);
|
|
89
|
+
out(`Account: ${key.accountName} (${key.accountId})`);
|
|
90
|
+
out(`Access: ${scope}`);
|
|
91
|
+
if (clients !== null)
|
|
92
|
+
out(`Clients: ${clients >= CLIENTS_COUNTED ? `${CLIENTS_COUNTED}+` : clients}`);
|
|
93
|
+
out(`WhatsApp Numbers: ${connected.length}${connected.length ? ` (${connected.filter(n => n.isActive).length} on)` : ''}`);
|
|
94
|
+
out(`Webhooks: ${subscriptions ? `${active} active of ${subscriptions.length}` : 'not visible with this key (pinned to My company)'}`);
|
|
95
|
+
}, next);
|
|
81
96
|
return 0;
|
|
82
97
|
}
|
|
83
98
|
async function health(ctx) {
|
|
@@ -86,7 +101,7 @@ async function health(ctx) {
|
|
|
86
101
|
ctx.output.out(result.healthy ? paint(ctx.output, 'green', 'Healthy.') : paint(ctx.output, 'red', 'Something needs your attention:'));
|
|
87
102
|
for (const alert of result.alerts)
|
|
88
103
|
ctx.output.out(` ${alert.level.padEnd(8)} ${alert.code} ${alert.message} (number ${alert.channelId})`);
|
|
89
|
-
});
|
|
104
|
+
}, result.healthy ? [] : [...new Set(result.alerts.map(a => `nodium numbers check ${a.channelId}`))]);
|
|
90
105
|
return result.healthy ? 0 : 1;
|
|
91
106
|
}
|
|
92
107
|
async function events(ctx) {
|
|
@@ -100,9 +115,7 @@ async function events(ctx) {
|
|
|
100
115
|
ctx.output.out(`The log is at event #${page.next_after}. Read what comes next with --after ${page.next_after}.`);
|
|
101
116
|
for (const line of table(page.data.map(e => [String(e.seq), when(e.occurred_at), e.event, e.id, e.external_id ?? ''])))
|
|
102
117
|
ctx.output.out(line);
|
|
103
|
-
|
|
104
|
-
ctx.output.out(paint(ctx.output, 'dim', `Next: --after ${page.next_after}`));
|
|
105
|
-
});
|
|
118
|
+
}, [`nodium events --after ${page.next_after}`]);
|
|
106
119
|
return 0;
|
|
107
120
|
}
|
|
108
121
|
export const ACCOUNT_COMMANDS = [
|
|
@@ -25,12 +25,12 @@ async function listConversations(ctx) {
|
|
|
25
25
|
c.window.open ? 'window open' : 'window closed', when(c.lastInboundAt)
|
|
26
26
|
])))
|
|
27
27
|
ctx.output.out(line);
|
|
28
|
-
});
|
|
28
|
+
}, items.length ? [`nodium messages list ${items[0].id}`] : ['nodium listen --forward <local url>']);
|
|
29
29
|
return 0;
|
|
30
30
|
}
|
|
31
31
|
async function getConversation(ctx) {
|
|
32
32
|
const conversation = await ctx.scope().conversations.get(ctx.args.positionals[0]);
|
|
33
|
-
ctx.print(conversation, () => details(conversation).forEach(line => ctx.output.out(line)));
|
|
33
|
+
ctx.print(conversation, () => details(conversation).forEach(line => ctx.output.out(line)), [`nodium messages list ${conversation.id}`]);
|
|
34
34
|
return 0;
|
|
35
35
|
}
|
|
36
36
|
async function listMessages(ctx) {
|
|
@@ -41,7 +41,7 @@ async function listMessages(ctx) {
|
|
|
41
41
|
when(m.createdAt), m.direction === 'inbound' ? '←' : '→', m.status, m.id, preview(m.textContent, m.type)
|
|
42
42
|
])))
|
|
43
43
|
ctx.output.out(line);
|
|
44
|
-
});
|
|
44
|
+
}, items.length ? [`nodium messages get ${items[0].id}`] : []);
|
|
45
45
|
return 0;
|
|
46
46
|
}
|
|
47
47
|
async function getMessage(ctx) {
|
|
@@ -58,12 +58,12 @@ async function listContacts(ctx) {
|
|
|
58
58
|
c.id, c.displayName ?? '—', c.phone ?? c.email ?? c.externalId ?? '—', c.optInAt ? 'opted in' : c.optOutAt ? 'opted out' : '', c.blocked ? 'blocked' : ''
|
|
59
59
|
])))
|
|
60
60
|
ctx.output.out(line);
|
|
61
|
-
});
|
|
61
|
+
}, items.length ? [`nodium contacts get ${items[0].id}`] : []);
|
|
62
62
|
return 0;
|
|
63
63
|
}
|
|
64
64
|
async function getContact(ctx) {
|
|
65
65
|
const contact = await ctx.scope().contacts.get(ctx.args.positionals[0]);
|
|
66
|
-
ctx.print(contact, () => details(contact).forEach(line => ctx.output.out(line)));
|
|
66
|
+
ctx.print(contact, () => details(contact).forEach(line => ctx.output.out(line)), [`nodium conversations list --contact ${contact.id}`]);
|
|
67
67
|
return 0;
|
|
68
68
|
}
|
|
69
69
|
const LIMIT_HELP = ' --limit <n> At most n items, newest first (default 20); pages are followed for you.';
|
package/dist/commands/flows.js
CHANGED
|
@@ -52,9 +52,11 @@ async function deploy(ctx) {
|
|
|
52
52
|
out(`${paint(ctx.output, 'green', 'Published')}: ${published.name} can now be sent.`);
|
|
53
53
|
}
|
|
54
54
|
else {
|
|
55
|
-
out(
|
|
55
|
+
out('Valid.');
|
|
56
56
|
}
|
|
57
|
-
}
|
|
57
|
+
}, errors.length
|
|
58
|
+
? [`nodium flows deploy ${file} --id ${flowId}`]
|
|
59
|
+
: published ? [] : [`nodium flows deploy ${file} --id ${flowId} --publish`]);
|
|
58
60
|
return errors.length ? 1 : 0;
|
|
59
61
|
}
|
|
60
62
|
export const FLOW_COMMANDS = [
|
package/dist/commands/numbers.js
CHANGED
|
@@ -9,15 +9,17 @@ async function list(ctx) {
|
|
|
9
9
|
const numbers = await ctx.scope().channels.list(hasFlag(ctx.args, 'all') ? { scope: 'account' } : {});
|
|
10
10
|
ctx.print(numbers, () => {
|
|
11
11
|
if (!numbers.length)
|
|
12
|
-
ctx.output.out('No number connected.
|
|
12
|
+
ctx.output.out('No number connected.');
|
|
13
13
|
for (const line of table(numbers.map(n => [n.id, n.identity, n.name, numberState(n) + (n.isTest ? ' · sandbox' : '')])))
|
|
14
14
|
ctx.output.out(line);
|
|
15
|
-
}
|
|
15
|
+
}, numbers.length
|
|
16
|
+
? [`nodium numbers check ${numbers[0].id}`, `nodium send --channel ${numbers[0].id} --to <+33…> --text "Hello"`]
|
|
17
|
+
: ['nodium connect-link new --tenant <id>']);
|
|
16
18
|
return 0;
|
|
17
19
|
}
|
|
18
20
|
async function get(ctx) {
|
|
19
21
|
const number = await ctx.scope().channels.get(ctx.args.positionals[0]);
|
|
20
|
-
ctx.print(number, () => details(number).forEach(line => ctx.output.out(line)));
|
|
22
|
+
ctx.print(number, () => details(number).forEach(line => ctx.output.out(line)), [`nodium numbers check ${number.id}`, `nodium send --channel ${number.id} --to <+33…> --text "Hello"`]);
|
|
21
23
|
return 0;
|
|
22
24
|
}
|
|
23
25
|
async function check(ctx) {
|
|
@@ -28,7 +30,7 @@ async function check(ctx) {
|
|
|
28
30
|
ctx.output.out(result.message);
|
|
29
31
|
if (result.number)
|
|
30
32
|
details(result.number).forEach(line => ctx.output.out(` ${line}`));
|
|
31
|
-
});
|
|
33
|
+
}, result.ok ? [`nodium send --channel ${ctx.args.positionals[0]} --to <+33…> --text "Hello"`] : ['nodium health']);
|
|
32
34
|
return result.ok ? 0 : 1;
|
|
33
35
|
}
|
|
34
36
|
export const NUMBER_COMMANDS = [
|
|
@@ -24,13 +24,23 @@ export function parseDuration(text) {
|
|
|
24
24
|
}
|
|
25
25
|
/** L'attente entre deux questions à Meta. */
|
|
26
26
|
export const WAIT_INTERVAL_MS = 15_000;
|
|
27
|
+
/** Ce qui suit un modèle, d'après son verdict chez Meta. */
|
|
28
|
+
function templateNext(id, name, outcome) {
|
|
29
|
+
if (outcome === 'approved')
|
|
30
|
+
return [`nodium send --channel <id> --to <+33…> --template ${name} --language <code>`];
|
|
31
|
+
if (outcome === 'not_deployed')
|
|
32
|
+
return [`nodium templates deploy ${id}`];
|
|
33
|
+
if (outcome === 'pending')
|
|
34
|
+
return [`nodium templates wait ${id}`];
|
|
35
|
+
return [`nodium templates update ${id} --input <file.json>`, `nodium templates deploy ${id}`];
|
|
36
|
+
}
|
|
27
37
|
async function list(ctx) {
|
|
28
38
|
const result = await ctx.scope().templates.list(hasFlag(ctx.args, 'all') ? { scope: 'account' } : {});
|
|
29
39
|
ctx.print(result, () => {
|
|
30
40
|
for (const line of table(result.templates.map(t => [t.id, t.name, t.language, t.category.toLowerCase(), t.status])))
|
|
31
41
|
ctx.output.out(line);
|
|
32
42
|
ctx.output.out(paint(ctx.output, 'dim', `${result.summary.total} templates: ${result.summary.deployed} deployed, ${result.summary.draft} drafts.`));
|
|
33
|
-
});
|
|
43
|
+
}, result.templates.length ? [`nodium templates get ${result.templates[0].id}`] : ['nodium templates new --input <file.json>']);
|
|
34
44
|
return 0;
|
|
35
45
|
}
|
|
36
46
|
async function get(ctx) {
|
|
@@ -41,7 +51,7 @@ async function get(ctx) {
|
|
|
41
51
|
ctx.output.out(paint(ctx.output, 'bold', 'At Meta'));
|
|
42
52
|
for (const line of table(result.deployments.map(d => [d.wabaId ?? '—', d.status, d.rejectionReason ?? '', when(d.reviewedAt)])))
|
|
43
53
|
ctx.output.out(` ${line}`);
|
|
44
|
-
});
|
|
54
|
+
}, templateNext(result.template.id, result.template.name, templateOutcome(result.deployments)));
|
|
45
55
|
return 0;
|
|
46
56
|
}
|
|
47
57
|
async function requireInput(ctx) {
|
|
@@ -54,13 +64,12 @@ async function create(ctx) {
|
|
|
54
64
|
const result = await ctx.scope().templates.create(await requireInput(ctx));
|
|
55
65
|
ctx.print(result, () => {
|
|
56
66
|
ctx.output.out(`${paint(ctx.output, 'green', 'Draft saved')}: ${result.template.name} (${result.template.id}). Nothing went to Meta yet.`);
|
|
57
|
-
|
|
58
|
-
});
|
|
67
|
+
}, [`nodium templates deploy ${result.template.id}`]);
|
|
59
68
|
return 0;
|
|
60
69
|
}
|
|
61
70
|
async function update(ctx) {
|
|
62
71
|
const result = await ctx.scope().templates.update(ctx.args.positionals[0], await requireInput(ctx));
|
|
63
|
-
ctx.print(result, () => ctx.output.out(`Saved: ${result.template.name} (${result.template.status}).`));
|
|
72
|
+
ctx.print(result, () => ctx.output.out(`Saved: ${result.template.name} (${result.template.status}).`), [`nodium templates deploy ${result.template.id}`]);
|
|
64
73
|
return 0;
|
|
65
74
|
}
|
|
66
75
|
async function deploy(ctx) {
|
|
@@ -71,21 +80,19 @@ async function deploy(ctx) {
|
|
|
71
80
|
ctx.print(result, () => {
|
|
72
81
|
for (const line of table(result.results.map(r => [r.clientName, r.wabaId ?? '—', r.status, r.reason ?? ''])))
|
|
73
82
|
ctx.output.out(line);
|
|
74
|
-
|
|
75
|
-
ctx.output.out(paint(ctx.output, 'dim', `Wait for Meta's review: nodium templates wait ${id}`));
|
|
76
|
-
});
|
|
83
|
+
}, refused ? [`nodium templates update ${id} --input <file.json>`] : [`nodium templates wait ${id}`]);
|
|
77
84
|
return refused ? 1 : 0;
|
|
78
85
|
}
|
|
79
86
|
async function refresh(ctx) {
|
|
80
87
|
const result = await ctx.scope().templates.refresh(ctx.args.positionals[0]);
|
|
81
|
-
ctx.print(result, () => ctx.output.out(`${result.template.name}: ${result.template.status} (${result.changed} changed).`));
|
|
88
|
+
ctx.print(result, () => ctx.output.out(`${result.template.name}: ${result.template.status} (${result.changed} changed).`), [`nodium templates get ${result.template.id}`]);
|
|
82
89
|
return 0;
|
|
83
90
|
}
|
|
84
91
|
async function remove(ctx) {
|
|
85
92
|
const id = ctx.args.positionals[0];
|
|
86
93
|
await ctx.confirm(`Delete template ${id}`);
|
|
87
94
|
const result = await ctx.scope().templates.delete(id);
|
|
88
|
-
ctx.print(result, () => ctx.output.out(result.deleted ? `Template ${id} deleted.` : `Template ${id} was not deleted.`));
|
|
95
|
+
ctx.print(result, () => ctx.output.out(result.deleted ? `Template ${id} deleted.` : `Template ${id} was not deleted.`), ['nodium templates list']);
|
|
89
96
|
return result.deleted ? 0 : 1;
|
|
90
97
|
}
|
|
91
98
|
async function wait(ctx) {
|
|
@@ -110,9 +117,8 @@ async function wait(ctx) {
|
|
|
110
117
|
err(paint(ctx.output, 'red', `${detail.template.name} was not approved.`));
|
|
111
118
|
for (const d of detail.deployments.filter(d => REFUSED.has(d.status)))
|
|
112
119
|
err(` ${d.wabaId ?? '—'} ${d.status} ${d.rejectionReason ?? ''}`);
|
|
113
|
-
err('Fix it with `nodium templates update`, then deploy again.');
|
|
114
120
|
}
|
|
115
|
-
});
|
|
121
|
+
}, templateNext(id, detail.template.name, outcome));
|
|
116
122
|
return outcome === 'approved' ? 0 : 1;
|
|
117
123
|
}
|
|
118
124
|
if (!ctx.json)
|
package/dist/commands/tenants.js
CHANGED
|
@@ -28,12 +28,14 @@ async function list(ctx) {
|
|
|
28
28
|
ctx.print(items, () => {
|
|
29
29
|
for (const line of table(items.map(t => [t.id, t.externalId ?? '—', t.name, `WhatsApp ${t.whatsapp.status.replace('_', ' ')}`, t.status])))
|
|
30
30
|
ctx.output.out(line);
|
|
31
|
-
});
|
|
31
|
+
}, items.length ? [`nodium tenants get ${items[0].externalId ?? items[0].id}`] : ['nodium tenants new --name "<client name>" --connect-link']);
|
|
32
32
|
return 0;
|
|
33
33
|
}
|
|
34
34
|
async function get(ctx) {
|
|
35
35
|
const tenant = await ctx.nodium().tenants.get(ctx.args.positionals[0]);
|
|
36
|
-
ctx.print(tenant, () => details(tenant).forEach(line => ctx.output.out(line))
|
|
36
|
+
ctx.print(tenant, () => details(tenant).forEach(line => ctx.output.out(line)), tenant.whatsapp.status === 'connected'
|
|
37
|
+
? [`nodium numbers list --tenant ${tenant.externalId ?? tenant.id}`]
|
|
38
|
+
: [`nodium connect-link new --tenant ${tenant.externalId ?? tenant.id}`]);
|
|
37
39
|
return 0;
|
|
38
40
|
}
|
|
39
41
|
async function create(ctx) {
|
|
@@ -50,14 +52,13 @@ async function create(ctx) {
|
|
|
50
52
|
ctx.print(tenant, () => {
|
|
51
53
|
ctx.output.out(`${paint(ctx.output, 'green', 'Client added')}: ${tenant.name} (${tenant.id}).`);
|
|
52
54
|
printLink(ctx, tenant.connectLink.url, tenant.connectLink.expiresAt);
|
|
53
|
-
});
|
|
55
|
+
}, [`nodium tenants get ${tenant.externalId ?? tenant.id}`]);
|
|
54
56
|
return 0;
|
|
55
57
|
}
|
|
56
58
|
const tenant = await tenants.create(input);
|
|
57
59
|
ctx.print(tenant, () => {
|
|
58
60
|
ctx.output.out(`${paint(ctx.output, 'green', 'Client added')}: ${tenant.name} (${tenant.id}).`);
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
+
}, [`nodium connect-link new --tenant ${tenant.externalId ?? tenant.id}`]);
|
|
61
62
|
return 0;
|
|
62
63
|
}
|
|
63
64
|
async function newLink(ctx) {
|
|
@@ -67,7 +68,7 @@ async function newLink(ctx) {
|
|
|
67
68
|
const input = linkInput(ctx);
|
|
68
69
|
await ctx.confirm(`Create a connect link for client ${tenant}`);
|
|
69
70
|
const link = await ctx.nodium().tenants.connectLink(tenant, input);
|
|
70
|
-
ctx.print(link, () => printLink(ctx, link.url, link.expiresAt));
|
|
71
|
+
ctx.print(link, () => printLink(ctx, link.url, link.expiresAt), [`nodium tenants get ${tenant}`]);
|
|
71
72
|
return 0;
|
|
72
73
|
}
|
|
73
74
|
const LINK_HELP = ` --redirect-url <https://…> Where your client lands once connected.
|
|
@@ -25,12 +25,14 @@ async function list(ctx) {
|
|
|
25
25
|
const result = await ctx.scope().webhooks.list();
|
|
26
26
|
ctx.print(result, () => {
|
|
27
27
|
if (!result.subscriptions.length)
|
|
28
|
-
ctx.output.out('No subscription.
|
|
28
|
+
ctx.output.out('No subscription.');
|
|
29
29
|
for (const line of table(result.subscriptions.map(s => [
|
|
30
30
|
s.id, s.isActive ? 'active' : `paused${s.disabledReason ? ` (${s.disabledReason})` : ''}`, s.url, s.events.join(','), s.lastError ? `last error: ${s.lastError}` : ''
|
|
31
31
|
])))
|
|
32
32
|
ctx.output.out(line);
|
|
33
|
-
}
|
|
33
|
+
}, result.subscriptions.length
|
|
34
|
+
? [`nodium webhooks test ${result.subscriptions[0].id}`, 'nodium webhooks deliveries --failed']
|
|
35
|
+
: ['nodium webhooks new --url <https://…> --event message.received', 'nodium listen --forward <local url>']);
|
|
34
36
|
return 0;
|
|
35
37
|
}
|
|
36
38
|
async function deliveries(ctx) {
|
|
@@ -47,9 +49,7 @@ async function deliveries(ctx) {
|
|
|
47
49
|
when(d.createdAt), d.id, d.event, d.status, d.responseStatus === null ? '—' : String(d.responseStatus), `${d.attempts} attempt${d.attempts === 1 ? '' : 's'}`, d.lastError ?? ''
|
|
48
50
|
])))
|
|
49
51
|
ctx.output.out(line);
|
|
50
|
-
|
|
51
|
-
ctx.output.out(paint(ctx.output, 'dim', 'Send one again: nodium webhooks redeliver <deliveryId>'));
|
|
52
|
-
});
|
|
52
|
+
}, items.find(d => d.status === 'failed') ? [`nodium webhooks redeliver ${items.find(d => d.status === 'failed').id}`] : []);
|
|
53
53
|
return 0;
|
|
54
54
|
}
|
|
55
55
|
async function create(ctx) {
|
|
@@ -66,8 +66,7 @@ async function create(ctx) {
|
|
|
66
66
|
ctx.print(result, () => {
|
|
67
67
|
ctx.output.out(`${paint(ctx.output, 'green', 'Subscribed')}: ${result.id}`);
|
|
68
68
|
printSecret(ctx, result.secret);
|
|
69
|
-
|
|
70
|
-
});
|
|
69
|
+
}, [`nodium webhooks test ${result.id}`]);
|
|
71
70
|
return 0;
|
|
72
71
|
}
|
|
73
72
|
async function update(ctx) {
|
|
@@ -93,14 +92,14 @@ async function update(ctx) {
|
|
|
93
92
|
throw new UsageError('Nothing to change: give --url, --event, --channel, --label or --active.');
|
|
94
93
|
await ctx.confirm(`Change webhook ${id} (${Object.keys(input).join(', ')})`);
|
|
95
94
|
const subscription = await ctx.scope().webhooks.update(id, input);
|
|
96
|
-
ctx.print(subscription, () => ctx.output.out(`Saved: ${subscription.id} → ${subscription.url} (${subscription.isActive ? 'active' : 'paused'}).`));
|
|
95
|
+
ctx.print(subscription, () => ctx.output.out(`Saved: ${subscription.id} → ${subscription.url} (${subscription.isActive ? 'active' : 'paused'}).`), [`nodium webhooks test ${subscription.id}`]);
|
|
97
96
|
return 0;
|
|
98
97
|
}
|
|
99
98
|
async function remove(ctx) {
|
|
100
99
|
const id = ctx.args.positionals[0];
|
|
101
100
|
await ctx.confirm(`Delete webhook ${id}: Nodium stops calling it`);
|
|
102
101
|
const result = await ctx.scope().webhooks.delete(id);
|
|
103
|
-
ctx.print(result ?? { deleted: true }, () => ctx.output.out(`Webhook ${id} deleted.`));
|
|
102
|
+
ctx.print(result ?? { deleted: true }, () => ctx.output.out(`Webhook ${id} deleted.`), ['nodium webhooks list']);
|
|
104
103
|
return 0;
|
|
105
104
|
}
|
|
106
105
|
async function test(ctx) {
|
|
@@ -109,14 +108,14 @@ async function test(ctx) {
|
|
|
109
108
|
ctx.output.out(result.ok
|
|
110
109
|
? `${paint(ctx.output, 'green', String(result.status))} answered in ${result.ms} ms.`
|
|
111
110
|
: `${paint(ctx.output, 'red', result.status === null ? 'ERR' : String(result.status))} ${result.error ?? 'not a 2xx answer'} (${result.ms} ms).`);
|
|
112
|
-
});
|
|
111
|
+
}, result.ok ? ['nodium webhooks deliveries'] : [`nodium webhooks update ${ctx.args.positionals[0]} --url <https://…>`]);
|
|
113
112
|
return result.ok ? 0 : 1;
|
|
114
113
|
}
|
|
115
114
|
async function redeliver(ctx) {
|
|
116
115
|
const id = ctx.args.positionals[0];
|
|
117
116
|
await ctx.confirm(`Send delivery ${id} again`);
|
|
118
117
|
const delivery = await ctx.scope().webhooks.redeliver(id);
|
|
119
|
-
ctx.print(delivery, () => ctx.output.out(`Delivery ${delivery.id}: ${delivery.status}${delivery.responseStatus ? ` (${delivery.responseStatus})` : ''}.`));
|
|
118
|
+
ctx.print(delivery, () => ctx.output.out(`Delivery ${delivery.id}: ${delivery.status}${delivery.responseStatus ? ` (${delivery.responseStatus})` : ''}.`), ['nodium webhooks deliveries']);
|
|
120
119
|
return delivery.status === 'failed' ? 1 : 0;
|
|
121
120
|
}
|
|
122
121
|
async function rotateSecret(ctx) {
|
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);
|
package/dist/run.js
CHANGED
|
@@ -6,7 +6,7 @@ import { COMMANDS, findCommands } from './commands/index.js';
|
|
|
6
6
|
import { configPath as defaultConfigPath, readConfig, resolveKey } from './config.js';
|
|
7
7
|
import { confirmWrite, outputMode } from './context.js';
|
|
8
8
|
import { pause } from './listen.js';
|
|
9
|
-
import { describeError, errorJson, paint, table } from './output.js';
|
|
9
|
+
import { describeError, errorJson, paint, table, printNext } from './output.js';
|
|
10
10
|
/**
|
|
11
11
|
* La commande `nodium` : lit la ligne de commande, cherche la commande dans la
|
|
12
12
|
* table (`commands/index.ts`), branche le client, rend le code de sortie
|
|
@@ -147,7 +147,12 @@ export async function run(argv, context) {
|
|
|
147
147
|
const tenant = flag(args, 'tenant');
|
|
148
148
|
return tenant ? nodium().as(tenant) : nodium();
|
|
149
149
|
},
|
|
150
|
-
print: (data, human
|
|
150
|
+
print: (data, human, next) => {
|
|
151
|
+
if (json)
|
|
152
|
+
return output.out(JSON.stringify(data, null, 2));
|
|
153
|
+
human();
|
|
154
|
+
printNext(output, next);
|
|
155
|
+
},
|
|
151
156
|
confirm: what => confirmWrite(what, { yes: hasFlag(args, 'yes'), interactive, ask: io.ask })
|
|
152
157
|
};
|
|
153
158
|
if (command.needsKey !== false)
|
package/dist/send.js
CHANGED
|
@@ -85,8 +85,7 @@ async function send(ctx) {
|
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
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
|
-
|
|
89
|
-
});
|
|
88
|
+
}, message ? [`nodium messages get ${message.id}`] : []);
|
|
90
89
|
return failed ? 1 : 0;
|
|
91
90
|
}
|
|
92
91
|
export const SEND_COMMAND = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodium.io/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
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",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"prepublishOnly": "npm run build"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@nodium.io/whatsapp": "^0.3.
|
|
20
|
+
"@nodium.io/whatsapp": "^0.3.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^26.4.0",
|