@manybot/manybot 5.7.0 → 5.9.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/README.md +28 -3
- package/dist/client/banner.js +10 -0
- package/dist/client/banner.test.js +31 -0
- package/dist/client/store.js +91 -6
- package/dist/client/store.test.js +170 -0
- package/dist/config.js +28 -44
- package/dist/config.test.js +26 -0
- package/dist/download/queue.js +13 -4
- package/dist/drivers/baileys/adapter.js +133 -15
- package/dist/drivers/baileys/api/contacts.integration.test.js +261 -0
- package/dist/drivers/baileys/api/groupMeta.test.js +235 -0
- package/dist/drivers/baileys/api/index.js +384 -62
- package/dist/drivers/baileys/index.js +92 -36
- package/dist/drivers/baileys/loginPrompt.js +0 -2
- package/dist/drivers/baileys/messageHandler.js +344 -4
- package/dist/drivers/baileys/messageHandler.test.js +445 -0
- package/dist/drivers/baileysAdapter.test.js +378 -0
- package/dist/drivers/jid.js +26 -0
- package/dist/drivers/jid.test.js +74 -0
- package/dist/drivers/types.js +5 -5
- package/dist/i18n/index.js +20 -24
- package/dist/kernel/activeDriverSend.js +21 -0
- package/dist/kernel/activeDriverSend.test.js +89 -0
- package/dist/kernel/alerts.js +3 -9
- package/dist/kernel/chatOverrides.js +46 -0
- package/dist/kernel/chatOverrides.test.js +59 -0
- package/dist/kernel/chatSession.js +65 -0
- package/dist/kernel/chatSession.test.js +46 -0
- package/dist/kernel/commandAccess.js +66 -0
- package/dist/kernel/commandAccess.test.js +74 -0
- package/dist/kernel/commandDeprecation.js +170 -0
- package/dist/kernel/commandDeprecation.test.js +114 -0
- package/dist/kernel/commandMenu.js +357 -0
- package/dist/kernel/commandMenu.test.js +363 -0
- package/dist/kernel/commandPermissions.js +171 -0
- package/dist/kernel/commandPermissions.test.js +227 -0
- package/dist/kernel/commandRegistry.js +583 -0
- package/dist/kernel/commandRegistry.test.js +158 -0
- package/dist/kernel/commandsConfig.js +949 -0
- package/dist/kernel/commandsConfig.test.js +482 -0
- package/dist/kernel/contactAutoSave.js +6 -6
- package/dist/kernel/contactAutoSave.test.js +87 -0
- package/dist/kernel/coreCommands.js +62 -0
- package/dist/kernel/driverManager.js +10 -6
- package/dist/kernel/driverManager.test.js +90 -0
- package/dist/kernel/integrationMode.js +88 -0
- package/dist/kernel/integrationMode.test.js +95 -0
- package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
- package/dist/kernel/pluginApi.test.js +600 -0
- package/dist/kernel/pluginGuard.js +18 -13
- package/dist/kernel/pluginGuard.test.js +39 -0
- package/dist/kernel/pluginLoader.js +169 -11
- package/dist/kernel/pluginLoader.test.js +190 -0
- package/dist/kernel/runCommand.js +284 -0
- package/dist/kernel/runCommand.test.js +497 -0
- package/dist/kernel/sendFallbackGuard.js +19 -48
- package/dist/kernel/sendFallbackGuard.test.js +80 -0
- package/dist/kernel/sendGuard.js +38 -42
- package/dist/kernel/sendGuard.test.js +102 -0
- package/dist/kernel/settingsDb.js +19 -5
- package/dist/kernel/statusServer.js +9 -2
- package/dist/kernel/statusServer.test.js +70 -0
- package/dist/kernel/testConfig.js +192 -0
- package/dist/kernel/testConfig.test.js +181 -0
- package/dist/kernel/updateCheck.js +33 -10
- package/dist/locales/en.json +77 -13
- package/dist/locales/es.json +77 -13
- package/dist/locales/pt.json +77 -13
- package/dist/logger/logger.js +23 -3
- package/dist/logger/logger.test.js +45 -0
- package/dist/main.js +5 -76
- package/dist/plugins/__manybot_integration__/index.js +184 -0
- package/dist/plugins/__manybot_integration__/index.test.js +218 -0
- package/dist/utils/phoneNumber.js +83 -0
- package/dist/utils/phoneNumber.test.js +53 -0
- package/package.json +76 -18
- package/dist/drivers/whatsmeow/client.js +0 -252
- package/dist/drivers/whatsmeow/index.js +0 -79
- package/dist/drivers/whatsmeow/installer.js +0 -86
- package/dist/drivers/whatsmeow/supervisor.js +0 -328
- package/dist/drivers/whatsmeow/whatsmeow.proto +0 -64
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commandRegistry.ts
|
|
3
|
+
*
|
|
4
|
+
* Merges commands.yaml user overrides with each active plugin's
|
|
5
|
+
* `commands` defaults into a single, in-memory registry — the single
|
|
6
|
+
* source of truth for command routing. No routing happens here yet
|
|
7
|
+
* (that's the next step); this module just builds the structure and
|
|
8
|
+
* can be exercised in isolation with a unit test.
|
|
9
|
+
*/
|
|
10
|
+
import { logger } from "#logger";
|
|
11
|
+
import { t } from "#i18n";
|
|
12
|
+
import { loadCommandsConfig, } from "./commandsConfig.js";
|
|
13
|
+
import { pluginRegistry, resolvePluginCommandHandler, } from "./pluginLoader.js";
|
|
14
|
+
import { getActiveDeprecation, syncCommandHistory } from "./commandDeprecation.js";
|
|
15
|
+
import { resolveCoreCommandHandler } from "./coreCommands.js";
|
|
16
|
+
export const DEFAULT_PERMISSION_MESSAGES = {
|
|
17
|
+
botNotAdmin: () => t("commandPermissions.botNotAdmin"),
|
|
18
|
+
senderNotAdmin: () => t("commandPermissions.senderNotAdmin"),
|
|
19
|
+
ownerOnly: () => t("commandPermissions.ownerOnly"),
|
|
20
|
+
donoOnly: () => t("commandPermissions.donoOnly"),
|
|
21
|
+
wrongScope: () => t("commandPermissions.wrongScope"),
|
|
22
|
+
cooldown: () => t("commandPermissions.cooldown"),
|
|
23
|
+
blacklist: () => t("commandPermissions.blacklist"),
|
|
24
|
+
allowedChats: () => t("commandPermissions.allowedChats"),
|
|
25
|
+
};
|
|
26
|
+
export function resolvePermissions(specPerms, specMsgs, pluginPerms, defaultsPerms, defaultsMsgs, fallbackScope) {
|
|
27
|
+
const admin = specPerms?.admin ?? pluginPerms?.admin ?? false;
|
|
28
|
+
const botAdmin = specPerms?.botAdmin ?? pluginPerms?.botAdmin ?? false;
|
|
29
|
+
const scope = specPerms?.scope ?? pluginPerms?.scope ?? fallbackScope ?? "any";
|
|
30
|
+
const owner = specPerms?.owner ?? pluginPerms?.owner ?? false;
|
|
31
|
+
const cooldownSeconds = specPerms?.cooldownSeconds ?? pluginPerms?.cooldownSeconds ?? defaultsPerms?.cooldownSeconds ?? 0;
|
|
32
|
+
// `dono` is YAML-only (no plugin-level equivalent). Per-spec dono
|
|
33
|
+
// wins; falls back to null = "no specific owner restriction" (the
|
|
34
|
+
// global OWNER_NUMBER in manybot.toml is independent).
|
|
35
|
+
const dono = specPerms?.dono ?? null;
|
|
36
|
+
// `allowed_chats:` is a closed list of JIDs the command may run in.
|
|
37
|
+
// Only set by the YAML spec; null = no chat restriction.
|
|
38
|
+
const allowedChats = specPerms?.allowedChats && specPerms.allowedChats.length > 0
|
|
39
|
+
? [...specPerms.allowedChats]
|
|
40
|
+
: null;
|
|
41
|
+
// `hidden_outside_scope:` mirrors `group_only` / `dm_only`. Compute it
|
|
42
|
+
// here so the resolved value is consistent across the menu and the
|
|
43
|
+
// permission check. spec wins over plugin: a plugin manifest that
|
|
44
|
+
// declares `group_only: true` is hidden from DMs unless the spec
|
|
45
|
+
// narrows it differently.
|
|
46
|
+
let hiddenOutsideScope = null;
|
|
47
|
+
const scopeSrc = specPerms?.scope ?? pluginPerms?.scope ?? null;
|
|
48
|
+
const hiddenFlag = specPerms?.hiddenOutsideScope ?? pluginPerms?.hiddenOutsideScope ?? false;
|
|
49
|
+
const groupOnlyFlag = specPerms?.groupOnly ?? pluginPerms?.groupOnly ?? false;
|
|
50
|
+
const dmOnlyFlag = specPerms?.dmOnly ?? pluginPerms?.dmOnly ?? false;
|
|
51
|
+
if (scopeSrc === "group")
|
|
52
|
+
hiddenOutsideScope = "group";
|
|
53
|
+
else if (scopeSrc === "dm")
|
|
54
|
+
hiddenOutsideScope = "dm";
|
|
55
|
+
else if (hiddenFlag === true || groupOnlyFlag === true)
|
|
56
|
+
hiddenOutsideScope = "group";
|
|
57
|
+
else if (dmOnlyFlag === true)
|
|
58
|
+
hiddenOutsideScope = "dm";
|
|
59
|
+
const rawWhitelist = specPerms?.whitelist ?? pluginPerms?.whitelist ?? defaultsPerms?.whitelist ?? null;
|
|
60
|
+
const rawBlacklist = specPerms?.blacklist ?? pluginPerms?.blacklist ?? defaultsPerms?.blacklist ?? null;
|
|
61
|
+
const whitelist = rawWhitelist
|
|
62
|
+
? {
|
|
63
|
+
groups: rawWhitelist.groups ? [...rawWhitelist.groups] : [],
|
|
64
|
+
users: rawWhitelist.users ? [...rawWhitelist.users] : [],
|
|
65
|
+
}
|
|
66
|
+
: null;
|
|
67
|
+
const blacklist = rawBlacklist
|
|
68
|
+
? {
|
|
69
|
+
groups: rawBlacklist.groups ? [...rawBlacklist.groups] : [],
|
|
70
|
+
users: rawBlacklist.users ? [...rawBlacklist.users] : [],
|
|
71
|
+
}
|
|
72
|
+
: null;
|
|
73
|
+
const messages = {
|
|
74
|
+
botNotAdmin: specMsgs?.botNotAdmin ?? defaultsMsgs?.botNotAdmin ?? DEFAULT_PERMISSION_MESSAGES.botNotAdmin(),
|
|
75
|
+
senderNotAdmin: specMsgs?.senderNotAdmin ?? defaultsMsgs?.senderNotAdmin ?? DEFAULT_PERMISSION_MESSAGES.senderNotAdmin(),
|
|
76
|
+
ownerOnly: specMsgs?.ownerOnly ?? defaultsMsgs?.ownerOnly ?? undefined,
|
|
77
|
+
donoOnly: specMsgs?.donoOnly ?? defaultsMsgs?.donoOnly ?? DEFAULT_PERMISSION_MESSAGES.donoOnly(),
|
|
78
|
+
wrongScope: specMsgs?.wrongScope ?? defaultsMsgs?.wrongScope ?? DEFAULT_PERMISSION_MESSAGES.wrongScope(),
|
|
79
|
+
cooldown: specMsgs?.cooldown ?? defaultsMsgs?.cooldown ?? DEFAULT_PERMISSION_MESSAGES.cooldown(),
|
|
80
|
+
blacklist: specMsgs?.blacklist ?? defaultsMsgs?.blacklist ?? DEFAULT_PERMISSION_MESSAGES.blacklist(),
|
|
81
|
+
allowedChats: specMsgs?.allowedChats ?? defaultsMsgs?.allowedChats ?? DEFAULT_PERMISSION_MESSAGES.allowedChats(),
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
admin,
|
|
85
|
+
botAdmin,
|
|
86
|
+
scope,
|
|
87
|
+
owner,
|
|
88
|
+
cooldownSeconds,
|
|
89
|
+
whitelist,
|
|
90
|
+
blacklist,
|
|
91
|
+
dono,
|
|
92
|
+
allowedChats,
|
|
93
|
+
hiddenOutsideScope,
|
|
94
|
+
messages,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function pluginCommandKey(pluginName, functionName) {
|
|
98
|
+
return `${pluginName}::${functionName}`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Plugin-provided command defaults (`export const commands = {...}`) are
|
|
102
|
+
* plain JS, not parsed/validated YAML — a plugin can hand us any runtime
|
|
103
|
+
* value regardless of the `PluginCommandDefault` compile-time type. `cmd`
|
|
104
|
+
* and `aliases` end up bound to SQLite params (deprecation lookups) and a
|
|
105
|
+
* non-string/undefined value there throws instead of failing gracefully.
|
|
106
|
+
* Sanitize before it ever reaches the registry.
|
|
107
|
+
*/
|
|
108
|
+
function sanitizePluginCmd(value) {
|
|
109
|
+
if (typeof value !== "string")
|
|
110
|
+
return null;
|
|
111
|
+
const trimmed = value.trim();
|
|
112
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
113
|
+
}
|
|
114
|
+
function sanitizePluginAliases(value) {
|
|
115
|
+
if (!Array.isArray(value))
|
|
116
|
+
return [];
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const item of value) {
|
|
119
|
+
if (typeof item !== "string")
|
|
120
|
+
continue;
|
|
121
|
+
const trimmed = item.trim();
|
|
122
|
+
if (trimmed.length > 0)
|
|
123
|
+
out.push(trimmed);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
function normalizePluginCommand(def) {
|
|
128
|
+
const handler = resolvePluginCommandHandler(def);
|
|
129
|
+
if (def && typeof def === "object") {
|
|
130
|
+
return {
|
|
131
|
+
cmd: sanitizePluginCmd(def.cmd),
|
|
132
|
+
aliases: sanitizePluginAliases(def.aliases),
|
|
133
|
+
desc: def.desc ?? null,
|
|
134
|
+
category: typeof def.category === "string" ? def.category : null,
|
|
135
|
+
manual: def.manual ?? null,
|
|
136
|
+
permissions: def.permissions ?? null,
|
|
137
|
+
handler,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Bare function (pure-function style — no built-in identity), or
|
|
141
|
+
// something malformed entirely (handler stays null in that case).
|
|
142
|
+
return { cmd: null, aliases: [], desc: null, category: null, manual: null, permissions: null, handler };
|
|
143
|
+
}
|
|
144
|
+
/** Whether a `commands[fn]` export is a shape worth remembering at all —
|
|
145
|
+
* a bare handler function, or an object (even one missing `handler`,
|
|
146
|
+
* which is only validated at dispatch time, same as before this
|
|
147
|
+
* module supported the bare-function shape). Anything else (a string,
|
|
148
|
+
* a number, null/undefined) is not a command definition. */
|
|
149
|
+
function isUsablePluginCommandExport(def) {
|
|
150
|
+
return typeof def === "function" || (def !== null && typeof def === "object");
|
|
151
|
+
}
|
|
152
|
+
function registerInvocation(byInvocation, text, entryId, isOverride) {
|
|
153
|
+
const existing = byInvocation.get(text);
|
|
154
|
+
if (existing === undefined) {
|
|
155
|
+
byInvocation.set(text, entryId);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (existing === entryId)
|
|
159
|
+
return true;
|
|
160
|
+
logger.warn(t("system.commandRegistryInvocationCollision", {
|
|
161
|
+
text,
|
|
162
|
+
winner: existing,
|
|
163
|
+
loser: entryId,
|
|
164
|
+
via: isOverride ? "yaml override" : "plugin default"
|
|
165
|
+
}));
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function registerInvocationWithDeprecationGuard(byInvocation, text, entryId, isOverride, notifyChanges) {
|
|
169
|
+
if (!notifyChanges) {
|
|
170
|
+
return registerInvocation(byInvocation, text, entryId, isOverride);
|
|
171
|
+
}
|
|
172
|
+
const deprecation = getActiveDeprecation(text);
|
|
173
|
+
if (deprecation && deprecation.id !== entryId) {
|
|
174
|
+
logger.warn(t("system.commandDeprecationReservedInvocation", {
|
|
175
|
+
text,
|
|
176
|
+
id: deprecation.id
|
|
177
|
+
}));
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return registerInvocation(byInvocation, text, entryId, isOverride);
|
|
181
|
+
}
|
|
182
|
+
export const DEFAULT_MENU_CONFIG = {
|
|
183
|
+
enabled: false,
|
|
184
|
+
title: "🤖 ManyBot — Menu",
|
|
185
|
+
intro: {
|
|
186
|
+
en: "Use {prefix}<command> to run it or {prefix}help <command> to view its manual.",
|
|
187
|
+
pt: "Use {prefix}<comando> para executar ou {prefix}help <comando> para ver o manual.",
|
|
188
|
+
es: "Usa {prefix}<comando> para ejecutarlo o {prefix}help <comando> para ver el manual.",
|
|
189
|
+
},
|
|
190
|
+
footer: null,
|
|
191
|
+
cmd: "menu",
|
|
192
|
+
aliases: ["help", "man", "menu", "bot", "?"],
|
|
193
|
+
notFoundFallback: false,
|
|
194
|
+
suggestSimilar: false,
|
|
195
|
+
suggestMaxDistance: 2,
|
|
196
|
+
welcomeMessage: null,
|
|
197
|
+
welcomeWindowDays: 3,
|
|
198
|
+
pageSize: 15,
|
|
199
|
+
};
|
|
200
|
+
function resolveCategoryHiddenInScope(category, categories) {
|
|
201
|
+
if (!category)
|
|
202
|
+
return null;
|
|
203
|
+
return categories[category]?.hiddenInScope ?? null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Resolve a `CommandSubcommandSpec` into the runtime `CommandSubcommand`.
|
|
207
|
+
*
|
|
208
|
+
* Function-chain inheritance: a sub with `spec.functions === null` (no
|
|
209
|
+
* override) inherits the parent's resolved chain. With an explicit list
|
|
210
|
+
* (even an empty one) it uses that. The primary `function` is set to the
|
|
211
|
+
* first non-empty entry, falling back to the parent's primary. Same
|
|
212
|
+
* convention for `loading:` — spec override wins, otherwise the parent's
|
|
213
|
+
* resolved spec carries through.
|
|
214
|
+
*
|
|
215
|
+
* Subcommand permissions inherit from the parent's resolved permissions
|
|
216
|
+
* unless the spec overrides them — `parent.permissions.scope` is passed
|
|
217
|
+
* as `fallbackScope` to close the category → command → subcommand chain.
|
|
218
|
+
*/
|
|
219
|
+
function buildSubcommandFromSpec(spec, parent, pluginName, defaultsByKey, defaults, manuals, loadingPresetOverrides) {
|
|
220
|
+
const manual = spec.manual ?? manuals[spec.id] ?? null;
|
|
221
|
+
const functions = spec.functions !== null ? [...spec.functions] : [...parent.functions];
|
|
222
|
+
const fnName = functions[0] ?? parent.function ?? "";
|
|
223
|
+
// Each sub can point at a *different* plugin function than its
|
|
224
|
+
// siblings — resolve this sub's own default permissions from its own
|
|
225
|
+
// primary function, not from whatever the parent happens to carry.
|
|
226
|
+
const subDef = pluginName && fnName
|
|
227
|
+
? defaultsByKey.get(pluginCommandKey(pluginName, fnName))?.norm ?? null
|
|
228
|
+
: null;
|
|
229
|
+
return {
|
|
230
|
+
id: spec.id,
|
|
231
|
+
cmd: spec.cmd,
|
|
232
|
+
aliases: [...spec.aliases],
|
|
233
|
+
desc: spec.desc,
|
|
234
|
+
manual,
|
|
235
|
+
function: fnName,
|
|
236
|
+
functions,
|
|
237
|
+
loading: spec.loading ?? parent.loading,
|
|
238
|
+
arguments: [...spec.arguments],
|
|
239
|
+
permissions: resolvePermissions(spec.permissions, spec.messages, subDef?.permissions ?? null, defaults.permissions, defaults.messages, parent.permissions.scope),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function buildSubcommandsFromSpecs(specs, parent, pluginName, defaultsByKey, defaults, manuals, loadingPresetOverrides) {
|
|
243
|
+
const out = {};
|
|
244
|
+
for (const spec of specs) {
|
|
245
|
+
const token = spec.cmd.toLowerCase();
|
|
246
|
+
if (out[token]) {
|
|
247
|
+
logger.warn(t("system.commandsConfigDuplicateSubcommand", {
|
|
248
|
+
id: parent.id,
|
|
249
|
+
cmd: spec.cmd
|
|
250
|
+
}));
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
out[token] = buildSubcommandFromSpec(spec, parent, pluginName, defaultsByKey, defaults, manuals, loadingPresetOverrides);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Chain inheritance for `loading:` is:
|
|
259
|
+
*
|
|
260
|
+
* defaults.loading → categoryLoading[cat] → spec.loading
|
|
261
|
+
*
|
|
262
|
+
* Returns the first non-null in that order. An inline `spec.loading`
|
|
263
|
+
* already won against any preset (the parser resolves preset names at
|
|
264
|
+
* parse time). Categories that don't declare `loading:` pass `null`
|
|
265
|
+
* through, so the defaults spec wins.
|
|
266
|
+
*/
|
|
267
|
+
function resolveLoadingChain(spec, categoryKey, categoryLoading, defaults) {
|
|
268
|
+
return spec
|
|
269
|
+
?? (categoryKey ? categoryLoading[categoryKey] ?? null : null)
|
|
270
|
+
?? defaults.loading
|
|
271
|
+
?? null;
|
|
272
|
+
}
|
|
273
|
+
export function buildCommandRegistry(specs, pluginRegistry, defaults = {
|
|
274
|
+
notifyChanges: true,
|
|
275
|
+
notifyPeriodDays: 7,
|
|
276
|
+
notifyMessage: null,
|
|
277
|
+
}, menu = { ...DEFAULT_MENU_CONFIG }, categories = {}, manuals = {}, loadingPresets = {}, categoryLoading = {}, prefix = null) {
|
|
278
|
+
const byId = new Map();
|
|
279
|
+
const byInvocation = new Map();
|
|
280
|
+
const defaultsByKey = new Map();
|
|
281
|
+
const coreFunctionNames = new Set();
|
|
282
|
+
for (const spec of specs ?? []) {
|
|
283
|
+
for (const fn of spec.functions)
|
|
284
|
+
coreFunctionNames.add(fn);
|
|
285
|
+
for (const sub of spec.subcommands) {
|
|
286
|
+
for (const fn of sub.functions ?? [])
|
|
287
|
+
coreFunctionNames.add(fn);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const allPlugins = new Map(pluginRegistry);
|
|
291
|
+
allPlugins.set("core", {
|
|
292
|
+
name: "core",
|
|
293
|
+
status: "active",
|
|
294
|
+
run: null,
|
|
295
|
+
setup: null,
|
|
296
|
+
commands: Object.fromEntries([...coreFunctionNames, "ping", "status"].map((name) => [
|
|
297
|
+
name,
|
|
298
|
+
{
|
|
299
|
+
handler: resolveCoreCommandHandler(name),
|
|
300
|
+
},
|
|
301
|
+
])),
|
|
302
|
+
exports: null,
|
|
303
|
+
error: null,
|
|
304
|
+
guardOptions: { timeout: false },
|
|
305
|
+
});
|
|
306
|
+
for (const plugin of allPlugins.values()) {
|
|
307
|
+
if (plugin.status !== "active")
|
|
308
|
+
continue;
|
|
309
|
+
if (!plugin.commands)
|
|
310
|
+
continue;
|
|
311
|
+
for (const [fn, def] of Object.entries(plugin.commands)) {
|
|
312
|
+
const id = pluginCommandKey(plugin.name, fn);
|
|
313
|
+
// Only a genuinely broken export (not a function and not an
|
|
314
|
+
// object at all) is unusable no matter what commands.yaml says
|
|
315
|
+
// later — that's the one case worth a warning + a full skip.
|
|
316
|
+
if (!isUsablePluginCommandExport(def)) {
|
|
317
|
+
logger.warn(t("system.commandRegistryInvalidPluginCommand", {
|
|
318
|
+
id,
|
|
319
|
+
plugin: plugin.name,
|
|
320
|
+
function: fn
|
|
321
|
+
}));
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const norm = normalizePluginCommand(def);
|
|
325
|
+
// Remembered regardless of whether the plugin gave it a `cmd` —
|
|
326
|
+
// a "pure function" export (bare handler, no built-in identity)
|
|
327
|
+
// is only reachable once a commands.yaml entry supplies `cmd`,
|
|
328
|
+
// but it still needs to be findable by pluginCommandKey() below.
|
|
329
|
+
// A malformed `cmd` (present but not a usable string) also lands
|
|
330
|
+
// here silently — not an error, just no auto-registration.
|
|
331
|
+
defaultsByKey.set(id, { plugin, fn, norm });
|
|
332
|
+
if (!norm.cmd)
|
|
333
|
+
continue; // no built-in identity — yaml-only until overridden
|
|
334
|
+
const manual = norm.manual ?? manuals[id] ?? manuals[norm.cmd] ?? null;
|
|
335
|
+
const category = norm.category;
|
|
336
|
+
const entry = {
|
|
337
|
+
id,
|
|
338
|
+
cmd: norm.cmd,
|
|
339
|
+
aliases: norm.aliases,
|
|
340
|
+
desc: norm.desc,
|
|
341
|
+
category,
|
|
342
|
+
group: null,
|
|
343
|
+
manual,
|
|
344
|
+
source: "plugin",
|
|
345
|
+
pluginName: plugin.name,
|
|
346
|
+
function: fn,
|
|
347
|
+
functions: [fn],
|
|
348
|
+
loading: resolveLoadingChain(null, category, categoryLoading, defaults),
|
|
349
|
+
handler: norm.handler,
|
|
350
|
+
text: null,
|
|
351
|
+
permissions: resolvePermissions(null, null, norm.permissions, defaults.permissions, defaults.messages, category ? categories[category]?.scope : null),
|
|
352
|
+
arguments: [],
|
|
353
|
+
subcommands: {},
|
|
354
|
+
categoryHiddenInScope: null,
|
|
355
|
+
hiddenOutsideScope: null,
|
|
356
|
+
};
|
|
357
|
+
entry.hiddenOutsideScope = entry.permissions.hiddenOutsideScope;
|
|
358
|
+
byId.set(id, entry);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (specs) {
|
|
362
|
+
for (const spec of specs) {
|
|
363
|
+
const primaryFn = spec.functions[0] ?? null;
|
|
364
|
+
if (spec.plugin && primaryFn) {
|
|
365
|
+
const key = pluginCommandKey(spec.plugin, primaryFn);
|
|
366
|
+
const pluginDefault = defaultsByKey.get(key);
|
|
367
|
+
if (!pluginDefault) {
|
|
368
|
+
logger.warn(t("system.commandRegistryOrphanEntry", {
|
|
369
|
+
id: spec.id,
|
|
370
|
+
plugin: spec.plugin,
|
|
371
|
+
function: primaryFn
|
|
372
|
+
}));
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const norm = pluginDefault.norm;
|
|
376
|
+
let existing = byId.get(key);
|
|
377
|
+
if (!existing) {
|
|
378
|
+
// Pure-function plugin export: no auto-registered entry yet
|
|
379
|
+
// (norm.cmd was null), so commands.yaml is the sole source of
|
|
380
|
+
// `cmd`. Without it there's nothing to invoke this command by.
|
|
381
|
+
const cmd = spec.cmd;
|
|
382
|
+
if (!cmd) {
|
|
383
|
+
logger.warn(t("system.commandRegistryOrphanEntry", {
|
|
384
|
+
id: spec.id,
|
|
385
|
+
plugin: spec.plugin,
|
|
386
|
+
function: primaryFn
|
|
387
|
+
}));
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
existing = {
|
|
391
|
+
id: key,
|
|
392
|
+
cmd,
|
|
393
|
+
aliases: [],
|
|
394
|
+
desc: null,
|
|
395
|
+
category: null,
|
|
396
|
+
group: null,
|
|
397
|
+
manual: null,
|
|
398
|
+
source: "plugin",
|
|
399
|
+
pluginName: spec.plugin,
|
|
400
|
+
function: primaryFn,
|
|
401
|
+
functions: [primaryFn],
|
|
402
|
+
loading: resolveLoadingChain(spec.loading, null, categoryLoading, defaults),
|
|
403
|
+
handler: norm.handler,
|
|
404
|
+
text: null,
|
|
405
|
+
permissions: resolvePermissions(null, null, null, defaults.permissions, defaults.messages, null),
|
|
406
|
+
arguments: [],
|
|
407
|
+
subcommands: {},
|
|
408
|
+
categoryHiddenInScope: null,
|
|
409
|
+
hiddenOutsideScope: null,
|
|
410
|
+
};
|
|
411
|
+
existing.hiddenOutsideScope = existing.permissions.hiddenOutsideScope;
|
|
412
|
+
byId.set(key, existing);
|
|
413
|
+
}
|
|
414
|
+
if (spec.cmd)
|
|
415
|
+
existing.cmd = spec.cmd;
|
|
416
|
+
if (spec.aliases.length > 0)
|
|
417
|
+
existing.aliases = [...spec.aliases];
|
|
418
|
+
if (spec.desc)
|
|
419
|
+
existing.desc = spec.desc;
|
|
420
|
+
if (spec.category)
|
|
421
|
+
existing.category = spec.category;
|
|
422
|
+
if (spec.group !== null)
|
|
423
|
+
existing.group = spec.group;
|
|
424
|
+
existing.manual = spec.manual ?? norm.manual ?? manuals[spec.id] ?? manuals[existing.cmd] ?? null;
|
|
425
|
+
existing.permissions = resolvePermissions(spec.permissions, spec.messages, norm.permissions, defaults.permissions, defaults.messages, existing.category ? categories[existing.category]?.scope : null);
|
|
426
|
+
// Function chain: explicit `functions:` wins; fall back to the
|
|
427
|
+
// single primary if the YAML only declared `function:` /
|
|
428
|
+
// `plugin:` shorthand. Empty list intentionally means "no
|
|
429
|
+
// handlers" — see `CommandSpec.functions` doc.
|
|
430
|
+
if (spec.functions.length > 0)
|
|
431
|
+
existing.functions = [...spec.functions];
|
|
432
|
+
else if (primaryFn)
|
|
433
|
+
existing.functions = [primaryFn];
|
|
434
|
+
existing.arguments = [...spec.arguments];
|
|
435
|
+
existing.categoryHiddenInScope = resolveCategoryHiddenInScope(existing.category, categories);
|
|
436
|
+
existing.hiddenOutsideScope = existing.permissions.hiddenOutsideScope;
|
|
437
|
+
existing.loading = resolveLoadingChain(spec.loading, existing.category, categoryLoading, defaults);
|
|
438
|
+
existing.subcommands = buildSubcommandsFromSpecs(spec.subcommands, existing, spec.plugin, defaultsByKey, defaults, manuals, loadingPresets);
|
|
439
|
+
}
|
|
440
|
+
else if (spec.plugin && spec.functions.length === 0) {
|
|
441
|
+
// Metadata-only plugin entry: no handler of its own. Either a
|
|
442
|
+
// parent container grouping subcommands that each declare their
|
|
443
|
+
// own `function:` (e.g. `todo:`), or a bare declarative entry
|
|
444
|
+
// with no subcommands at all — either way `functions` stays
|
|
445
|
+
// empty and runCommand() short-circuits to "no_dispatch" (see
|
|
446
|
+
// its `fnNames.length === 0` check).
|
|
447
|
+
const id = spec.subcommands.length > 0 ? `parent::${spec.id}` : spec.id;
|
|
448
|
+
const entry = {
|
|
449
|
+
id,
|
|
450
|
+
cmd: spec.cmd,
|
|
451
|
+
aliases: [...spec.aliases],
|
|
452
|
+
desc: spec.desc,
|
|
453
|
+
category: spec.category ?? null,
|
|
454
|
+
group: spec.group,
|
|
455
|
+
manual: spec.manual ?? manuals[spec.id] ?? manuals[spec.cmd] ?? null,
|
|
456
|
+
source: "plugin",
|
|
457
|
+
pluginName: spec.plugin,
|
|
458
|
+
function: null,
|
|
459
|
+
functions: [],
|
|
460
|
+
loading: resolveLoadingChain(spec.loading, spec.category ?? null, categoryLoading, defaults),
|
|
461
|
+
handler: null,
|
|
462
|
+
text: null,
|
|
463
|
+
permissions: resolvePermissions(spec.permissions, spec.messages, null, defaults.permissions, defaults.messages, spec.category ? categories[spec.category]?.scope : null),
|
|
464
|
+
arguments: [...spec.arguments],
|
|
465
|
+
subcommands: {},
|
|
466
|
+
categoryHiddenInScope: resolveCategoryHiddenInScope(spec.category, categories),
|
|
467
|
+
hiddenOutsideScope: resolvePermissions(spec.permissions, spec.messages, null, defaults.permissions, defaults.messages, spec.category ? categories[spec.category]?.scope : null).hiddenOutsideScope,
|
|
468
|
+
};
|
|
469
|
+
entry.subcommands = buildSubcommandsFromSpecs(spec.subcommands, entry, spec.plugin, defaultsByKey, defaults, manuals, loadingPresets);
|
|
470
|
+
byId.set(id, entry);
|
|
471
|
+
}
|
|
472
|
+
else if (!spec.plugin) {
|
|
473
|
+
if (!spec.text) {
|
|
474
|
+
logger.warn(t("system.commandRegistryInvalidEntry", {
|
|
475
|
+
id: spec.id
|
|
476
|
+
}));
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const id = `text::${spec.id}`;
|
|
480
|
+
const manual = spec.manual ?? manuals[spec.id] ?? manuals[spec.cmd] ?? null;
|
|
481
|
+
const entryPerms = resolvePermissions(spec.permissions, spec.messages, null, defaults.permissions, defaults.messages, spec.category ? categories[spec.category]?.scope : null);
|
|
482
|
+
const entry = {
|
|
483
|
+
id,
|
|
484
|
+
cmd: spec.cmd,
|
|
485
|
+
aliases: [...spec.aliases],
|
|
486
|
+
desc: spec.desc,
|
|
487
|
+
category: spec.category ?? null,
|
|
488
|
+
group: spec.group,
|
|
489
|
+
manual,
|
|
490
|
+
source: "text",
|
|
491
|
+
pluginName: null,
|
|
492
|
+
function: null,
|
|
493
|
+
functions: [],
|
|
494
|
+
loading: resolveLoadingChain(spec.loading, spec.category ?? null, categoryLoading, defaults),
|
|
495
|
+
handler: null,
|
|
496
|
+
text: spec.text,
|
|
497
|
+
permissions: entryPerms,
|
|
498
|
+
arguments: [...spec.arguments],
|
|
499
|
+
subcommands: {},
|
|
500
|
+
categoryHiddenInScope: resolveCategoryHiddenInScope(spec.category, categories),
|
|
501
|
+
hiddenOutsideScope: entryPerms.hiddenOutsideScope,
|
|
502
|
+
};
|
|
503
|
+
entry.subcommands = buildSubcommandsFromSpecs(spec.subcommands, entry, null, defaultsByKey, defaults, manuals, loadingPresets);
|
|
504
|
+
byId.set(id, entry);
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// spec.plugin set, but neither `function:` nor a non-empty
|
|
508
|
+
// `subcommands:` block — nothing tells the kernel what to run.
|
|
509
|
+
logger.warn(t("system.commandRegistryInvalidEntry", {
|
|
510
|
+
id: spec.id
|
|
511
|
+
}));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const entry of byId.values()) {
|
|
516
|
+
registerInvocationWithDeprecationGuard(byInvocation, entry.cmd, entry.id, false, defaults.notifyChanges);
|
|
517
|
+
for (const alias of entry.aliases) {
|
|
518
|
+
registerInvocationWithDeprecationGuard(byInvocation, alias, entry.id, false, defaults.notifyChanges);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Validate menu cmd + aliases against command invocations. Fully opt-in:
|
|
522
|
+
// when menu.enabled is false (default), the native menu claims nothing —
|
|
523
|
+
// "menu"/"help" stay free for a plugin (legacy or commands.yaml) to use.
|
|
524
|
+
const menuAliases = new Set();
|
|
525
|
+
if (menu.enabled) {
|
|
526
|
+
const menuInvocations = [menu.cmd, ...menu.aliases.filter(a => a !== menu.cmd)];
|
|
527
|
+
for (const alias of menuInvocations) {
|
|
528
|
+
const existing = byInvocation.get(alias);
|
|
529
|
+
if (existing !== undefined) {
|
|
530
|
+
logger.warn(t("system.commandRegistryMenuAliasCollision", {
|
|
531
|
+
alias,
|
|
532
|
+
winner: existing
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
menuAliases.add(alias);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return { byId, byInvocation, defaults, menu, menuAliases, categories, manuals, loadingPresets, categoryLoading, prefix };
|
|
541
|
+
}
|
|
542
|
+
let currentRegistry = null;
|
|
543
|
+
export async function initCommandRegistry() {
|
|
544
|
+
const config = await loadCommandsConfig(new Set(pluginRegistry.keys()));
|
|
545
|
+
const defaults = config?.defaults ?? {
|
|
546
|
+
notifyChanges: true,
|
|
547
|
+
notifyPeriodDays: 7,
|
|
548
|
+
notifyMessage: null,
|
|
549
|
+
};
|
|
550
|
+
const menu = config?.menu ?? { ...DEFAULT_MENU_CONFIG };
|
|
551
|
+
const categories = config?.categories ?? {};
|
|
552
|
+
const manuals = config?.manuals ?? {};
|
|
553
|
+
const loadingPresets = config?.loadingPresets ?? {};
|
|
554
|
+
const categoryLoading = config?.categoryLoading ?? {};
|
|
555
|
+
const prefix = config?.prefix ?? null;
|
|
556
|
+
const specs = config?.specs ?? [];
|
|
557
|
+
const registry = buildCommandRegistry(specs, pluginRegistry, defaults, menu, categories, manuals, loadingPresets, categoryLoading, prefix);
|
|
558
|
+
syncCommandHistory(registry.byId, defaults, specs);
|
|
559
|
+
currentRegistry = registry;
|
|
560
|
+
return registry;
|
|
561
|
+
}
|
|
562
|
+
export function getCommandRegistry() {
|
|
563
|
+
return currentRegistry;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Test-only: inject a registry built with {@link buildCommandRegistry}
|
|
567
|
+
* directly, bypassing `initCommandRegistry()`'s disk config + real
|
|
568
|
+
* `pluginRegistry` load. Lets `runCommand.test.ts` exercise
|
|
569
|
+
* `resolveDispatch()`/`runCommand()`, which read the module-singleton
|
|
570
|
+
* `currentRegistry` and have no injectable-registry parameter (unlike
|
|
571
|
+
* `commandAccess.ts`'s functions). Pass `null` to reset.
|
|
572
|
+
*/
|
|
573
|
+
export function __setRegistryForTests(registry) {
|
|
574
|
+
currentRegistry = registry;
|
|
575
|
+
}
|
|
576
|
+
export function getCommandByInvocation(command) {
|
|
577
|
+
if (!currentRegistry)
|
|
578
|
+
return null;
|
|
579
|
+
const id = currentRegistry.byInvocation.get(command);
|
|
580
|
+
if (id === undefined)
|
|
581
|
+
return null;
|
|
582
|
+
return currentRegistry.byId.get(id) ?? null;
|
|
583
|
+
}
|