wldx-admin-cli 0.0.0-stage → 1.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 +404 -2
- package/bin/wx-admin.mjs +5 -0
- package/package.json +36 -3
- package/src/cli.mjs +71 -0
- package/src/commands/acl.mjs +376 -0
- package/src/commands/api.mjs +66 -0
- package/src/commands/config.mjs +280 -0
- package/src/commands/device.mjs +275 -0
- package/src/commands/dialplan.mjs +488 -0
- package/src/commands/index.mjs +85 -0
- package/src/commands/pbx.mjs +197 -0
- package/src/commands/phonebook.mjs +558 -0
- package/src/commands/queue.mjs +456 -0
- package/src/commands/records.mjs +623 -0
- package/src/commands/routing.mjs +1059 -0
- package/src/commands/sound.mjs +232 -0
- package/src/commands/system.mjs +668 -0
- package/src/commands/trunk.mjs +808 -0
- package/src/commands/user.mjs +574 -0
- package/src/match.mjs +15 -0
- package/src/pbx.mjs +369 -0
- package/src/secret.mjs +42 -0
- package/src/version.mjs +3 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// wx-admin sound: the sound files of a PBX (music on hold, ring tones, prompts) and their folders.
|
|
2
|
+
// Operations (PBX REST API; Company API Key scope sounds):
|
|
3
|
+
// GET /Sounds/{path}/ a folder: records [{name, path, size, isDir, isDefault, modified, formats}]
|
|
4
|
+
// (the root is /Sounds/; a folder lists ".." first)
|
|
5
|
+
// POST /Sounds/ multipart: file (the sound; its file name names it), directory
|
|
6
|
+
// PUT /Sounds/{path}/ {name} renames a sound, {move: folder} moves it
|
|
7
|
+
// DELETE /Sounds/{path}/ delete a sound
|
|
8
|
+
// POST /Sounds/directory/ {path} makes a folder (letters, digits and _ only)
|
|
9
|
+
// PUT /Sounds/directory/{path}/ {name} renames a folder, {move: folder} moves it
|
|
10
|
+
// DELETE /Sounds/directory/{path}/ delete a folder
|
|
11
|
+
// The PBX converts an uploaded sound to its own formats (a .wav is listed as .opus with ALAW, G729… versions).
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import {UsageError} from 'wldx-core/errors';
|
|
14
|
+
import {readForUpload} from 'wldx-core/files';
|
|
15
|
+
import {localTime, paint, table} from 'wldx-core/format';
|
|
16
|
+
import {pbxClient} from '../pbx.mjs';
|
|
17
|
+
import {segmentsPath} from './records.mjs';
|
|
18
|
+
|
|
19
|
+
const SCOPE = {scope: 'sounds'};
|
|
20
|
+
const FOLDER = /^[A-Za-z0-9_]{1,64}$/;
|
|
21
|
+
|
|
22
|
+
const TYPES = {
|
|
23
|
+
'.wav': 'audio/wav',
|
|
24
|
+
'.mp3': 'audio/mpeg',
|
|
25
|
+
'.opus': 'audio/opus',
|
|
26
|
+
'.ogg': 'audio/ogg',
|
|
27
|
+
'.gsm': 'audio/gsm',
|
|
28
|
+
'.alaw': 'audio/basic',
|
|
29
|
+
'.ulaw': 'audio/basic',
|
|
30
|
+
'.g729': 'audio/g729',
|
|
31
|
+
'.sln': 'audio/basic',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const trimSlashes = (p) => String(p ?? '').replace(/^\/+|\/+$/g, '');
|
|
35
|
+
|
|
36
|
+
// /Sounds/ or /Sounds/<encoded path>/
|
|
37
|
+
const soundsPath = (p) => (trimSlashes(p) ? `/Sounds/${segmentsPath(trimSlashes(p), 'sound path')}/` : '/Sounds/');
|
|
38
|
+
|
|
39
|
+
const checkFolder = (p, what = 'folder') => {
|
|
40
|
+
const parts = trimSlashes(p).split('/');
|
|
41
|
+
if (!trimSlashes(p) || parts.some((s) => !FOLDER.test(s)))
|
|
42
|
+
throw new UsageError(`A ${what} name is letters, digits and _ (the PBX allows no other): ${p}`);
|
|
43
|
+
return parts.join('/');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const listFolder = async (client, folder) =>
|
|
47
|
+
(await client.call('GET', soundsPath(folder), undefined, SCOPE))?.records ?? [];
|
|
48
|
+
|
|
49
|
+
const withoutExt = (p) => p.replace(/\.[A-Za-z0-9]{1,5}$/, '');
|
|
50
|
+
|
|
51
|
+
// The entry of a sound or folder, found in its parent folder: {path, name, isDir, …}.
|
|
52
|
+
async function entryOf(client, ref) {
|
|
53
|
+
const full = trimSlashes(ref);
|
|
54
|
+
if (!full) throw new UsageError('Give the path of a sound or folder (wx-admin sound list)');
|
|
55
|
+
const at = full.lastIndexOf('/');
|
|
56
|
+
const parent = at < 0 ? '' : full.slice(0, at);
|
|
57
|
+
const last = full.slice(at + 1);
|
|
58
|
+
const entries = (await listFolder(client, parent)).filter((e) => e.name !== '..');
|
|
59
|
+
const found =
|
|
60
|
+
entries.find((e) => e.path === full) ??
|
|
61
|
+
entries.find((e) => withoutExt(e.path) === withoutExt(full)) ??
|
|
62
|
+
entries.find((e) => e.name === last);
|
|
63
|
+
if (!found)
|
|
64
|
+
throw Object.assign(
|
|
65
|
+
new Error(`No sound or folder ${full} on this PBX (wx-admin sound list${parent ? ` ${parent}` : ''})`),
|
|
66
|
+
{status: 404},
|
|
67
|
+
);
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const itemPath = (e) => (e.isDir ? `/Sounds/directory/${segmentsPath(e.path, 'folder')}/` : soundsPath(e.path));
|
|
72
|
+
|
|
73
|
+
const sizeText = (bytes) =>
|
|
74
|
+
bytes >= 1048576
|
|
75
|
+
? `${(bytes / 1048576).toFixed(1)} MB`
|
|
76
|
+
: bytes >= 1024
|
|
77
|
+
? `${Math.round(bytes / 1024)} KB`
|
|
78
|
+
: `${bytes} B`;
|
|
79
|
+
|
|
80
|
+
export const groups = {
|
|
81
|
+
sound: {
|
|
82
|
+
summary: 'sound files (music on hold, ring tones, prompts) and their folders: list, upload, rename, move, delete',
|
|
83
|
+
commands: {
|
|
84
|
+
list: {
|
|
85
|
+
summary: 'the sounds and folders of a folder (the top one by default)',
|
|
86
|
+
usage: 'wx-admin sound list [<folder>]',
|
|
87
|
+
options: {},
|
|
88
|
+
args: [0, 1],
|
|
89
|
+
async run(ctx) {
|
|
90
|
+
const records = (await listFolder(await pbxClient(ctx), ctx.positionals[0] ?? '')).filter(
|
|
91
|
+
(e) => e.name !== '..',
|
|
92
|
+
);
|
|
93
|
+
return {folder: trimSlashes(ctx.positionals[0] ?? ''), records};
|
|
94
|
+
},
|
|
95
|
+
text(r, ctx) {
|
|
96
|
+
const c = paint(ctx);
|
|
97
|
+
if (!r.records.length) return `No sounds in ${r.folder || 'the top folder'}`;
|
|
98
|
+
const rows = [...r.records].sort((a, b) => Number(b.isDir) - Number(a.isDir) || a.name.localeCompare(b.name));
|
|
99
|
+
return table(
|
|
100
|
+
rows.map((e) => [
|
|
101
|
+
e.isDir ? c.blue(`${e.path}/`) : e.path,
|
|
102
|
+
e.isDir ? '' : sizeText(e.size ?? 0),
|
|
103
|
+
e.modified ? localTime(e.modified * 1000) : '',
|
|
104
|
+
e.isDefault ? c.dim('default') : '',
|
|
105
|
+
]),
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
upload: {
|
|
110
|
+
summary: 'upload a sound file (the PBX converts it to its formats)',
|
|
111
|
+
usage: 'wx-admin sound upload <file> [--to FOLDER] [--name NAME]',
|
|
112
|
+
details:
|
|
113
|
+
'A sound of the same name in that folder is replaced. The name is the file name without its extension unless --name gives one.',
|
|
114
|
+
options: {
|
|
115
|
+
to: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
placeholder: 'FOLDER',
|
|
118
|
+
description: 'the folder of the PBX to put it in (default: the top one)',
|
|
119
|
+
},
|
|
120
|
+
name: {type: 'string', placeholder: 'NAME', description: 'the name of the sound (letters, digits, _ and -)'},
|
|
121
|
+
},
|
|
122
|
+
args: [1, 1],
|
|
123
|
+
async run(ctx) {
|
|
124
|
+
const local = path.resolve(ctx.cwd ?? process.cwd(), ctx.positionals[0]);
|
|
125
|
+
const ext = path.extname(local).toLowerCase();
|
|
126
|
+
const name = ctx.values.name ?? path.basename(local, path.extname(local));
|
|
127
|
+
if (!/^[A-Za-z0-9_-]{1,100}$/.test(name))
|
|
128
|
+
throw new UsageError(
|
|
129
|
+
`A sound name is letters, digits, _ and -: ${name}${ctx.values.name ? '' : ' (rename the file, or give --name)'}`,
|
|
130
|
+
);
|
|
131
|
+
const folder = ctx.values.to === undefined || !trimSlashes(ctx.values.to) ? '' : checkFolder(ctx.values.to);
|
|
132
|
+
const bytes = readForUpload(local, ctx.env);
|
|
133
|
+
const form = new FormData();
|
|
134
|
+
form.append('file', new Blob([bytes], {type: TYPES[ext] ?? 'application/octet-stream'}), `${name}${ext}`);
|
|
135
|
+
form.append('directory', folder);
|
|
136
|
+
await (await pbxClient(ctx)).request('POST', '/Sounds/', {data: form, scope: 'sounds', timeoutMs: 300_000});
|
|
137
|
+
return {name, folder, file: local, bytes: bytes.length};
|
|
138
|
+
},
|
|
139
|
+
text: (r) => `Uploaded ${r.folder ? `${r.folder}/` : ''}${r.name} (${sizeText(r.bytes)})`,
|
|
140
|
+
},
|
|
141
|
+
rename: {
|
|
142
|
+
summary: 'rename a sound or a folder',
|
|
143
|
+
usage: 'wx-admin sound rename <path> <new-name>',
|
|
144
|
+
options: {},
|
|
145
|
+
args: [2, 2],
|
|
146
|
+
async run(ctx) {
|
|
147
|
+
const [ref, name] = ctx.positionals;
|
|
148
|
+
const client = await pbxClient(ctx);
|
|
149
|
+
const e = await entryOf(client, ref);
|
|
150
|
+
if (e.isDir) checkFolder(name, 'folder');
|
|
151
|
+
else if (!/^[A-Za-z0-9_-]{1,100}$/.test(name))
|
|
152
|
+
throw new UsageError(`A sound name is letters, digits, _ and - (no extension): ${name}`);
|
|
153
|
+
await client.call('PUT', itemPath(e), {name}, SCOPE);
|
|
154
|
+
return {path: e.path, folder: Boolean(e.isDir), name};
|
|
155
|
+
},
|
|
156
|
+
text: (r) => `Renamed ${r.folder ? 'the folder' : 'the sound'} ${r.path} to ${r.name}`,
|
|
157
|
+
},
|
|
158
|
+
move: {
|
|
159
|
+
summary: 'move a sound or a folder into another folder',
|
|
160
|
+
usage: 'wx-admin sound move <path> <folder>',
|
|
161
|
+
options: {},
|
|
162
|
+
args: [2, 2],
|
|
163
|
+
async run(ctx) {
|
|
164
|
+
const [ref, to] = ctx.positionals;
|
|
165
|
+
const folder = checkFolder(to);
|
|
166
|
+
const client = await pbxClient(ctx);
|
|
167
|
+
const e = await entryOf(client, ref);
|
|
168
|
+
await client.call('PUT', itemPath(e), {move: folder}, SCOPE);
|
|
169
|
+
return {path: e.path, folder: Boolean(e.isDir), to: folder};
|
|
170
|
+
},
|
|
171
|
+
text: (r) => `Moved ${r.folder ? 'the folder' : 'the sound'} ${r.path} to ${r.to}/`,
|
|
172
|
+
},
|
|
173
|
+
delete: {
|
|
174
|
+
summary: 'delete sounds (needs --yes)',
|
|
175
|
+
usage: 'wx-admin sound delete <path>... --yes',
|
|
176
|
+
details:
|
|
177
|
+
'Dialplans, queues or users that play a deleted sound play nothing instead. A folder goes with sound rmdir.',
|
|
178
|
+
options: {yes: {type: 'boolean', description: 'yes, delete them'}},
|
|
179
|
+
args: [1, 50],
|
|
180
|
+
async run(ctx) {
|
|
181
|
+
if (!ctx.values.yes)
|
|
182
|
+
throw new UsageError(
|
|
183
|
+
'A deleted sound cannot be brought back, and what plays it plays nothing: confirm with --yes',
|
|
184
|
+
ctx.usage(),
|
|
185
|
+
);
|
|
186
|
+
const client = await pbxClient(ctx);
|
|
187
|
+
const entries = [];
|
|
188
|
+
for (const ref of ctx.positionals) {
|
|
189
|
+
const e = await entryOf(client, ref);
|
|
190
|
+
if (e.isDir) throw new UsageError(`${e.path} is a folder: wx-admin sound rmdir ${e.path} --yes`);
|
|
191
|
+
if (e.isDefault) throw new UsageError(`${e.path} is a default sound of the PBX: it is not deleted`);
|
|
192
|
+
entries.push(e);
|
|
193
|
+
}
|
|
194
|
+
for (const e of entries) await client.call('DELETE', soundsPath(e.path), undefined, SCOPE);
|
|
195
|
+
return {deleted: entries.map((e) => e.path)};
|
|
196
|
+
},
|
|
197
|
+
text: (r) => `Deleted ${r.deleted.join(', ')}`,
|
|
198
|
+
},
|
|
199
|
+
mkdir: {
|
|
200
|
+
summary: 'make a folder for sounds',
|
|
201
|
+
usage: 'wx-admin sound mkdir <folder>',
|
|
202
|
+
details: 'A folder name is letters, digits and _ (the PBX allows no other).',
|
|
203
|
+
options: {},
|
|
204
|
+
args: [1, 1],
|
|
205
|
+
async run(ctx) {
|
|
206
|
+
const folder = checkFolder(ctx.positionals[0]);
|
|
207
|
+
await (await pbxClient(ctx)).call('POST', '/Sounds/directory/', {path: folder}, SCOPE);
|
|
208
|
+
return {folder};
|
|
209
|
+
},
|
|
210
|
+
text: (r) => `Made the folder ${r.folder}/`,
|
|
211
|
+
},
|
|
212
|
+
rmdir: {
|
|
213
|
+
summary: 'delete a folder with the sounds in it (needs --yes)',
|
|
214
|
+
usage: 'wx-admin sound rmdir <folder> --yes',
|
|
215
|
+
options: {yes: {type: 'boolean', description: 'yes, delete it and its sounds'}},
|
|
216
|
+
args: [1, 1],
|
|
217
|
+
async run(ctx) {
|
|
218
|
+
if (!ctx.values.yes)
|
|
219
|
+
throw new UsageError('Deleting a folder deletes the sounds in it: confirm with --yes', ctx.usage());
|
|
220
|
+
const client = await pbxClient(ctx);
|
|
221
|
+
const e = await entryOf(client, ctx.positionals[0]);
|
|
222
|
+
if (!e.isDir) throw new UsageError(`${e.path} is a sound: wx-admin sound delete ${e.path} --yes`);
|
|
223
|
+
if (e.isDefault) throw new UsageError(`${e.path} is a default folder of the PBX: it is not deleted`);
|
|
224
|
+
const inside = (await listFolder(client, e.path)).filter((x) => x.name !== '..').length;
|
|
225
|
+
await client.call('DELETE', itemPath(e), undefined, SCOPE);
|
|
226
|
+
return {folder: e.path, sounds: inside};
|
|
227
|
+
},
|
|
228
|
+
text: (r) => `Deleted the folder ${r.folder}/${r.sounds ? ` and the ${r.sounds} sounds or folders in it` : ''}`,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
};
|