@openvcs/sdk 0.2.10 → 0.2.11
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/lib/runtime/menu.d.ts +17 -0
- package/lib/runtime/menu.js +281 -22
- package/lib/runtime/registration.js +28 -1
- package/package.json +1 -1
- package/src/lib/runtime/menu.ts +339 -20
- package/src/lib/runtime/registration.ts +32 -1
package/lib/runtime/menu.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { MenubarItem } from '../types/menubar.js';
|
|
2
|
+
import type { PluginDelegates } from '../types/plugin.js';
|
|
3
|
+
import type { PluginRuntimeContext } from './contracts.js';
|
|
2
4
|
type MenubarMenuOptions = {
|
|
3
5
|
before?: string;
|
|
4
6
|
after?: string;
|
|
5
7
|
};
|
|
8
|
+
/** Runs one registered action handler by id. */
|
|
9
|
+
export declare function runRegisteredAction(actionId: string, ...args: unknown[]): Promise<boolean>;
|
|
6
10
|
export interface MenuHandle {
|
|
7
11
|
id: string;
|
|
8
12
|
addItem(item: MenubarItem): void;
|
|
@@ -11,15 +15,28 @@ export interface MenuHandle {
|
|
|
11
15
|
hideItem(actionId: string): void;
|
|
12
16
|
showItem(actionId: string): void;
|
|
13
17
|
}
|
|
18
|
+
/** Returns a menu by id, or null when it does not exist. */
|
|
14
19
|
export declare function getMenu(menuId: string): MenuHandle | null;
|
|
20
|
+
/** Returns a menu by id, creating it when necessary. */
|
|
15
21
|
export declare function getOrCreateMenu(menuId: string, label: string): MenuHandle | null;
|
|
22
|
+
/** Creates a menu at a specific insertion point. */
|
|
16
23
|
export declare function createMenu(menuId: string, label: string, options?: MenubarMenuOptions): MenuHandle | null;
|
|
24
|
+
/** Adds one action item to a menu. */
|
|
17
25
|
export declare function addMenuItem(menuId: string, item: MenubarItem): void;
|
|
26
|
+
/** Adds one separator item to a menu. */
|
|
18
27
|
export declare function addMenuSeparator(menuId: string, beforeAction?: string): void;
|
|
28
|
+
/** Removes one menu entirely. */
|
|
19
29
|
export declare function removeMenu(menuId: string): void;
|
|
30
|
+
/** Hides one menu from the host UI. */
|
|
20
31
|
export declare function hideMenu(menuId: string): void;
|
|
32
|
+
/** Shows one previously hidden menu. */
|
|
21
33
|
export declare function showMenu(menuId: string): void;
|
|
34
|
+
/** Registers an action handler by id. */
|
|
22
35
|
export declare function registerAction(id: string, handler: (...args: unknown[]) => unknown): void;
|
|
36
|
+
/** Invokes a host command when the host exposes a direct invoke helper. */
|
|
23
37
|
export declare function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
|
|
38
|
+
/** Emits a user notification when the host exposes one. */
|
|
24
39
|
export declare function notify(msg: string): void;
|
|
40
|
+
/** Builds the SDK plugin delegates contributed by the menu registry. */
|
|
41
|
+
export declare function createMenuPluginDelegates(): PluginDelegates<PluginRuntimeContext>;
|
|
25
42
|
export {};
|
package/lib/runtime/menu.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright © 2025-2026 OpenVCS Contributors
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.runRegisteredAction = runRegisteredAction;
|
|
5
6
|
exports.getMenu = getMenu;
|
|
6
7
|
exports.getOrCreateMenu = getOrCreateMenu;
|
|
7
8
|
exports.createMenu = createMenu;
|
|
@@ -13,56 +14,298 @@ exports.showMenu = showMenu;
|
|
|
13
14
|
exports.registerAction = registerAction;
|
|
14
15
|
exports.invoke = invoke;
|
|
15
16
|
exports.notify = notify;
|
|
17
|
+
exports.createMenuPluginDelegates = createMenuPluginDelegates;
|
|
18
|
+
const menus = new Map();
|
|
19
|
+
const menuOrder = [];
|
|
20
|
+
const actionHandlers = new Map();
|
|
21
|
+
let nextSyntheticId = 0;
|
|
22
|
+
/** Returns the OpenVCS global, when the host exposes one. */
|
|
16
23
|
function getOpenVCS() {
|
|
17
24
|
return globalThis.OpenVCS;
|
|
18
25
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
/** Normalizes menu ids for stable map lookups. */
|
|
27
|
+
function normalizeMenuId(menuId) {
|
|
28
|
+
return String(menuId || '').trim();
|
|
29
|
+
}
|
|
30
|
+
/** Allocates a stable synthetic id for a generated menu item. */
|
|
31
|
+
function allocateSyntheticId(prefix) {
|
|
32
|
+
nextSyntheticId += 1;
|
|
33
|
+
return `${prefix}-${nextSyntheticId}`;
|
|
34
|
+
}
|
|
35
|
+
/** Returns a stored menu state or null if it does not exist. */
|
|
36
|
+
function getStoredMenu(menuId) {
|
|
37
|
+
return menus.get(normalizeMenuId(menuId)) || null;
|
|
38
|
+
}
|
|
39
|
+
/** Inserts a menu id into the ordering list. */
|
|
40
|
+
function placeMenuId(menuId, options) {
|
|
41
|
+
const id = normalizeMenuId(menuId);
|
|
42
|
+
const existingIndex = menuOrder.indexOf(id);
|
|
43
|
+
if (existingIndex >= 0) {
|
|
44
|
+
menuOrder.splice(existingIndex, 1);
|
|
45
|
+
}
|
|
46
|
+
const beforeId = normalizeMenuId(options?.before || '');
|
|
47
|
+
const afterId = normalizeMenuId(options?.after || '');
|
|
48
|
+
if (afterId) {
|
|
49
|
+
const afterIndex = menuOrder.indexOf(afterId);
|
|
50
|
+
if (afterIndex >= 0) {
|
|
51
|
+
menuOrder.splice(afterIndex + 1, 0, id);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (beforeId) {
|
|
56
|
+
const beforeIndex = menuOrder.indexOf(beforeId);
|
|
57
|
+
if (beforeIndex >= 0) {
|
|
58
|
+
menuOrder.splice(beforeIndex, 0, id);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
menuOrder.push(id);
|
|
63
|
+
}
|
|
64
|
+
/** Ensures a menu record exists for the provided id. */
|
|
65
|
+
function ensureStoredMenu(menuId, label, options) {
|
|
66
|
+
const id = normalizeMenuId(menuId);
|
|
67
|
+
const safeLabel = String(label || '').trim() || id;
|
|
68
|
+
let menu = menus.get(id);
|
|
69
|
+
if (!menu) {
|
|
70
|
+
menu = { id, label: safeLabel, items: [] };
|
|
71
|
+
menus.set(id, menu);
|
|
72
|
+
placeMenuId(id, options);
|
|
73
|
+
return menu;
|
|
74
|
+
}
|
|
75
|
+
menu.label = safeLabel;
|
|
76
|
+
placeMenuId(id, options);
|
|
22
77
|
return menu;
|
|
23
78
|
}
|
|
79
|
+
/** Finds one menu item by action id. */
|
|
80
|
+
function findMenuItem(menu, actionId) {
|
|
81
|
+
const id = normalizeMenuId(actionId);
|
|
82
|
+
return menu.items.find((item) => item.action === id) || null;
|
|
83
|
+
}
|
|
84
|
+
/** Inserts a menu item relative to before/after anchors when provided. */
|
|
85
|
+
function insertMenuItem(menu, item, before, after) {
|
|
86
|
+
const beforeId = normalizeMenuId(before || '');
|
|
87
|
+
const afterId = normalizeMenuId(after || '');
|
|
88
|
+
const insertAt = (index) => {
|
|
89
|
+
const existingIndex = menu.items.findIndex((entry) => entry.id === item.id || entry.action === item.action);
|
|
90
|
+
if (existingIndex >= 0) {
|
|
91
|
+
menu.items.splice(existingIndex, 1);
|
|
92
|
+
}
|
|
93
|
+
menu.items.splice(index, 0, item);
|
|
94
|
+
};
|
|
95
|
+
if (beforeId) {
|
|
96
|
+
const beforeIndex = menu.items.findIndex((entry) => entry.action === beforeId);
|
|
97
|
+
if (beforeIndex >= 0) {
|
|
98
|
+
insertAt(beforeIndex);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (afterId) {
|
|
103
|
+
const afterIndex = menu.items.findIndex((entry) => entry.action === afterId);
|
|
104
|
+
if (afterIndex >= 0) {
|
|
105
|
+
insertAt(afterIndex + 1);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
insertAt(menu.items.length);
|
|
110
|
+
}
|
|
111
|
+
/** Converts one stored menu item into a serializable plugin payload element. */
|
|
112
|
+
function serializeMenuItem(item) {
|
|
113
|
+
if (item.hidden)
|
|
114
|
+
return null;
|
|
115
|
+
if (item.kind === 'separator') {
|
|
116
|
+
return {
|
|
117
|
+
type: 'text',
|
|
118
|
+
id: item.id,
|
|
119
|
+
content: item.content || '—',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (item.kind === 'text') {
|
|
123
|
+
return {
|
|
124
|
+
type: 'text',
|
|
125
|
+
id: item.id,
|
|
126
|
+
content: item.content || item.label,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
type: 'button',
|
|
131
|
+
id: item.action || item.id,
|
|
132
|
+
label: item.label,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Serializes the current menu registry for the host runtime. */
|
|
136
|
+
function serializeMenus() {
|
|
137
|
+
return menuOrder
|
|
138
|
+
.map((menuId, index) => {
|
|
139
|
+
const menu = menus.get(menuId);
|
|
140
|
+
if (!menu || menu.hidden)
|
|
141
|
+
return null;
|
|
142
|
+
return {
|
|
143
|
+
id: menu.id,
|
|
144
|
+
label: menu.label,
|
|
145
|
+
order: index + 1,
|
|
146
|
+
elements: menu.items
|
|
147
|
+
.map((item) => serializeMenuItem(item))
|
|
148
|
+
.filter((item) => Boolean(item)),
|
|
149
|
+
};
|
|
150
|
+
})
|
|
151
|
+
.filter((menu) => Boolean(menu));
|
|
152
|
+
}
|
|
153
|
+
/** Runs one registered action handler by id. */
|
|
154
|
+
async function runRegisteredAction(actionId, ...args) {
|
|
155
|
+
const id = String(actionId || '').trim();
|
|
156
|
+
if (!id)
|
|
157
|
+
return false;
|
|
158
|
+
const handler = actionHandlers.get(id);
|
|
159
|
+
if (!handler)
|
|
160
|
+
return false;
|
|
161
|
+
await handler(...args);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
/** Creates a stable, mutation-based handle for one menu id. */
|
|
165
|
+
function createMenuHandle(menuId) {
|
|
166
|
+
const id = normalizeMenuId(menuId);
|
|
167
|
+
return {
|
|
168
|
+
id,
|
|
169
|
+
addItem(item) {
|
|
170
|
+
const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
|
|
171
|
+
const label = String(item?.label || '').trim();
|
|
172
|
+
const action = String(item?.action || '').trim();
|
|
173
|
+
if (!label || !action)
|
|
174
|
+
return;
|
|
175
|
+
const entry = {
|
|
176
|
+
kind: 'button',
|
|
177
|
+
id: action,
|
|
178
|
+
label,
|
|
179
|
+
title: item.title,
|
|
180
|
+
action,
|
|
181
|
+
};
|
|
182
|
+
insertMenuItem(menu, entry, item.before, item.after);
|
|
183
|
+
const openvcs = getOpenVCS();
|
|
184
|
+
if (!openvcs?.menus)
|
|
185
|
+
return;
|
|
186
|
+
openvcs.menus.getOrCreate(menu.id, menu.label);
|
|
187
|
+
openvcs.menus.addMenuItem(menu.id, item);
|
|
188
|
+
},
|
|
189
|
+
addSeparator(beforeAction) {
|
|
190
|
+
const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
|
|
191
|
+
const entry = {
|
|
192
|
+
kind: 'separator',
|
|
193
|
+
id: allocateSyntheticId(`${menu.id}-separator`),
|
|
194
|
+
label: 'Separator',
|
|
195
|
+
content: '—',
|
|
196
|
+
};
|
|
197
|
+
insertMenuItem(menu, entry, beforeAction);
|
|
198
|
+
const openvcs = getOpenVCS();
|
|
199
|
+
if (!openvcs?.menus)
|
|
200
|
+
return;
|
|
201
|
+
openvcs.menus.getOrCreate(menu.id, menu.label);
|
|
202
|
+
openvcs.menus.addMenuSeparator(menu.id, beforeAction);
|
|
203
|
+
},
|
|
204
|
+
removeItem(actionId) {
|
|
205
|
+
const menu = getStoredMenu(this.id);
|
|
206
|
+
if (!menu)
|
|
207
|
+
return;
|
|
208
|
+
const idToRemove = normalizeMenuId(actionId);
|
|
209
|
+
menu.items = menu.items.filter((item) => item.action !== idToRemove);
|
|
210
|
+
const openvcs = getOpenVCS();
|
|
211
|
+
if (openvcs?.menus) {
|
|
212
|
+
const handle = openvcs.menus.get(this.id);
|
|
213
|
+
handle?.removeItem(idToRemove);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
hideItem(actionId) {
|
|
217
|
+
const menu = getStoredMenu(this.id);
|
|
218
|
+
if (!menu)
|
|
219
|
+
return;
|
|
220
|
+
const item = findMenuItem(menu, actionId);
|
|
221
|
+
if (item)
|
|
222
|
+
item.hidden = true;
|
|
223
|
+
const openvcs = getOpenVCS();
|
|
224
|
+
if (!openvcs?.menus)
|
|
225
|
+
return;
|
|
226
|
+
const handle = openvcs.menus.get(this.id);
|
|
227
|
+
handle?.hideItem(actionId);
|
|
228
|
+
},
|
|
229
|
+
showItem(actionId) {
|
|
230
|
+
const menu = getStoredMenu(this.id);
|
|
231
|
+
if (!menu)
|
|
232
|
+
return;
|
|
233
|
+
const item = findMenuItem(menu, actionId);
|
|
234
|
+
if (item)
|
|
235
|
+
item.hidden = false;
|
|
236
|
+
const openvcs = getOpenVCS();
|
|
237
|
+
if (!openvcs?.menus)
|
|
238
|
+
return;
|
|
239
|
+
const handle = openvcs.menus.get(this.id);
|
|
240
|
+
handle?.showItem(actionId);
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Returns a menu by id, or null when it does not exist. */
|
|
24
245
|
function getMenu(menuId) {
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
246
|
+
const stored = getStoredMenu(menuId);
|
|
247
|
+
if (!stored)
|
|
27
248
|
return null;
|
|
28
|
-
return
|
|
249
|
+
return createMenuHandle(stored.id);
|
|
29
250
|
}
|
|
251
|
+
/** Returns a menu by id, creating it when necessary. */
|
|
30
252
|
function getOrCreateMenu(menuId, label) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
return null;
|
|
34
|
-
return castMenuHandle(openvcs.menus.getOrCreate(menuId, label));
|
|
253
|
+
const stored = ensureStoredMenu(menuId, label);
|
|
254
|
+
return createMenuHandle(stored.id);
|
|
35
255
|
}
|
|
256
|
+
/** Creates a menu at a specific insertion point. */
|
|
36
257
|
function createMenu(menuId, label, options) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
return null;
|
|
40
|
-
return castMenuHandle(openvcs.menus.create(menuId, label, options));
|
|
258
|
+
const stored = ensureStoredMenu(menuId, label, options);
|
|
259
|
+
return createMenuHandle(stored.id);
|
|
41
260
|
}
|
|
261
|
+
/** Adds one action item to a menu. */
|
|
42
262
|
function addMenuItem(menuId, item) {
|
|
43
|
-
const
|
|
44
|
-
|
|
263
|
+
const menu = getStoredMenu(menuId) || ensureStoredMenu(menuId, menuId);
|
|
264
|
+
const handle = createMenuHandle(menu.id);
|
|
265
|
+
handle.addItem(item);
|
|
45
266
|
}
|
|
267
|
+
/** Adds one separator item to a menu. */
|
|
46
268
|
function addMenuSeparator(menuId, beforeAction) {
|
|
47
|
-
const
|
|
48
|
-
|
|
269
|
+
const menu = getStoredMenu(menuId) || ensureStoredMenu(menuId, menuId);
|
|
270
|
+
const handle = createMenuHandle(menu.id);
|
|
271
|
+
handle.addSeparator(beforeAction);
|
|
49
272
|
}
|
|
273
|
+
/** Removes one menu entirely. */
|
|
50
274
|
function removeMenu(menuId) {
|
|
275
|
+
const id = normalizeMenuId(menuId);
|
|
276
|
+
menus.delete(id);
|
|
277
|
+
const index = menuOrder.indexOf(id);
|
|
278
|
+
if (index >= 0)
|
|
279
|
+
menuOrder.splice(index, 1);
|
|
51
280
|
const openvcs = getOpenVCS();
|
|
52
|
-
openvcs?.menus?.remove(
|
|
281
|
+
openvcs?.menus?.remove(id);
|
|
53
282
|
}
|
|
283
|
+
/** Hides one menu from the host UI. */
|
|
54
284
|
function hideMenu(menuId) {
|
|
285
|
+
const menu = getStoredMenu(menuId);
|
|
286
|
+
if (menu)
|
|
287
|
+
menu.hidden = true;
|
|
55
288
|
const openvcs = getOpenVCS();
|
|
56
|
-
openvcs?.menus?.hide(menuId);
|
|
289
|
+
openvcs?.menus?.hide(normalizeMenuId(menuId));
|
|
57
290
|
}
|
|
291
|
+
/** Shows one previously hidden menu. */
|
|
58
292
|
function showMenu(menuId) {
|
|
293
|
+
const menu = getStoredMenu(menuId);
|
|
294
|
+
if (menu)
|
|
295
|
+
menu.hidden = false;
|
|
59
296
|
const openvcs = getOpenVCS();
|
|
60
|
-
openvcs?.menus?.show(menuId);
|
|
297
|
+
openvcs?.menus?.show(normalizeMenuId(menuId));
|
|
61
298
|
}
|
|
299
|
+
/** Registers an action handler by id. */
|
|
62
300
|
function registerAction(id, handler) {
|
|
301
|
+
const key = String(id || '').trim();
|
|
302
|
+
if (!key)
|
|
303
|
+
return;
|
|
304
|
+
actionHandlers.set(key, handler);
|
|
63
305
|
const openvcs = getOpenVCS();
|
|
64
|
-
openvcs?.registerAction(
|
|
306
|
+
openvcs?.registerAction(key, handler);
|
|
65
307
|
}
|
|
308
|
+
/** Invokes a host command when the host exposes a direct invoke helper. */
|
|
66
309
|
function invoke(cmd, args) {
|
|
67
310
|
const openvcs = getOpenVCS();
|
|
68
311
|
if (!openvcs) {
|
|
@@ -70,7 +313,23 @@ function invoke(cmd, args) {
|
|
|
70
313
|
}
|
|
71
314
|
return openvcs.invoke(cmd, args);
|
|
72
315
|
}
|
|
316
|
+
/** Emits a user notification when the host exposes one. */
|
|
73
317
|
function notify(msg) {
|
|
74
318
|
const openvcs = getOpenVCS();
|
|
75
319
|
openvcs?.notify(msg);
|
|
76
320
|
}
|
|
321
|
+
/** Builds the SDK plugin delegates contributed by the menu registry. */
|
|
322
|
+
function createMenuPluginDelegates() {
|
|
323
|
+
return {
|
|
324
|
+
async 'plugin.get_menus'() {
|
|
325
|
+
return serializeMenus();
|
|
326
|
+
},
|
|
327
|
+
async 'plugin.handle_action'(params) {
|
|
328
|
+
const actionId = String(params?.action_id || '').trim();
|
|
329
|
+
if (!actionId)
|
|
330
|
+
return null;
|
|
331
|
+
await runRegisteredAction(actionId);
|
|
332
|
+
return null;
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
@@ -5,10 +5,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
5
5
|
exports.createRegisteredPluginRuntime = createRegisteredPluginRuntime;
|
|
6
6
|
exports.bootstrapPluginModule = bootstrapPluginModule;
|
|
7
7
|
const factory_1 = require("./factory");
|
|
8
|
+
const menu_1 = require("./menu");
|
|
8
9
|
/** Creates a runtime from one declarative plugin module definition. */
|
|
9
10
|
function createRegisteredPluginRuntime(definition = {}) {
|
|
11
|
+
const menuDelegates = (0, menu_1.createMenuPluginDelegates)();
|
|
12
|
+
const explicitPluginDelegates = definition.plugin ?? {};
|
|
13
|
+
const explicitGetMenus = explicitPluginDelegates['plugin.get_menus'];
|
|
14
|
+
const explicitHandleAction = explicitPluginDelegates['plugin.handle_action'];
|
|
10
15
|
const options = {
|
|
11
|
-
plugin:
|
|
16
|
+
plugin: {
|
|
17
|
+
...explicitPluginDelegates,
|
|
18
|
+
'plugin.get_menus': async (params, ctx) => {
|
|
19
|
+
const menus = await menuDelegates['plugin.get_menus']?.(params, ctx);
|
|
20
|
+
const explicitMenus = explicitGetMenus
|
|
21
|
+
? await explicitGetMenus(params, ctx)
|
|
22
|
+
: [];
|
|
23
|
+
return [
|
|
24
|
+
...(Array.isArray(explicitMenus) ? explicitMenus : []),
|
|
25
|
+
...(Array.isArray(menus) ? menus : []),
|
|
26
|
+
];
|
|
27
|
+
},
|
|
28
|
+
'plugin.handle_action': async (params, ctx) => {
|
|
29
|
+
const actionId = String(params?.action_id || '').trim();
|
|
30
|
+
if (actionId && (await (0, menu_1.runRegisteredAction)(actionId))) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (explicitHandleAction) {
|
|
34
|
+
return explicitHandleAction(params, ctx);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
12
39
|
vcs: definition.vcs,
|
|
13
40
|
implements: definition.implements,
|
|
14
41
|
logTarget: definition.logTarget,
|
package/package.json
CHANGED
package/src/lib/runtime/menu.ts
CHANGED
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
import type { MenubarItem } from '../types/menubar.js';
|
|
5
|
+
import type {
|
|
6
|
+
PluginDelegates,
|
|
7
|
+
PluginHandleActionParams,
|
|
8
|
+
PluginMenuDefinition,
|
|
9
|
+
} from '../types/plugin.js';
|
|
10
|
+
|
|
11
|
+
import type { PluginRuntimeContext } from './contracts.js';
|
|
5
12
|
|
|
6
13
|
type MenubarMenuOptions = { before?: string; after?: string };
|
|
7
14
|
|
|
15
|
+
type MenuEntryKind = 'button' | 'text' | 'separator';
|
|
16
|
+
|
|
8
17
|
type OpenVCSGlobal = typeof globalThis & {
|
|
9
18
|
OpenVCS?: {
|
|
10
19
|
menus?: {
|
|
@@ -23,10 +32,206 @@ type OpenVCSGlobal = typeof globalThis & {
|
|
|
23
32
|
};
|
|
24
33
|
};
|
|
25
34
|
|
|
35
|
+
interface StoredMenuItem {
|
|
36
|
+
kind: MenuEntryKind;
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
content?: string;
|
|
41
|
+
action?: string;
|
|
42
|
+
hidden?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface StoredMenuState {
|
|
46
|
+
id: string;
|
|
47
|
+
label: string;
|
|
48
|
+
hidden?: boolean;
|
|
49
|
+
items: StoredMenuItem[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SerializedMenuItem {
|
|
53
|
+
type: 'button' | 'text';
|
|
54
|
+
id: string;
|
|
55
|
+
label?: string;
|
|
56
|
+
content?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SerializedMenuDefinition {
|
|
60
|
+
id: string;
|
|
61
|
+
label: string;
|
|
62
|
+
order: number;
|
|
63
|
+
elements: SerializedMenuItem[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const menus = new Map<string, StoredMenuState>();
|
|
67
|
+
const menuOrder: string[] = [];
|
|
68
|
+
const actionHandlers = new Map<string, (...args: unknown[]) => unknown>();
|
|
69
|
+
let nextSyntheticId = 0;
|
|
70
|
+
|
|
71
|
+
/** Returns the OpenVCS global, when the host exposes one. */
|
|
26
72
|
function getOpenVCS() {
|
|
27
73
|
return (globalThis as OpenVCSGlobal).OpenVCS;
|
|
28
74
|
}
|
|
29
75
|
|
|
76
|
+
/** Normalizes menu ids for stable map lookups. */
|
|
77
|
+
function normalizeMenuId(menuId: string): string {
|
|
78
|
+
return String(menuId || '').trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Allocates a stable synthetic id for a generated menu item. */
|
|
82
|
+
function allocateSyntheticId(prefix: string): string {
|
|
83
|
+
nextSyntheticId += 1;
|
|
84
|
+
return `${prefix}-${nextSyntheticId}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Returns a stored menu state or null if it does not exist. */
|
|
88
|
+
function getStoredMenu(menuId: string): StoredMenuState | null {
|
|
89
|
+
return menus.get(normalizeMenuId(menuId)) || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Inserts a menu id into the ordering list. */
|
|
93
|
+
function placeMenuId(menuId: string, options?: MenubarMenuOptions): void {
|
|
94
|
+
const id = normalizeMenuId(menuId);
|
|
95
|
+
const existingIndex = menuOrder.indexOf(id);
|
|
96
|
+
if (existingIndex >= 0) {
|
|
97
|
+
menuOrder.splice(existingIndex, 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const beforeId = normalizeMenuId(options?.before || '');
|
|
101
|
+
const afterId = normalizeMenuId(options?.after || '');
|
|
102
|
+
|
|
103
|
+
if (afterId) {
|
|
104
|
+
const afterIndex = menuOrder.indexOf(afterId);
|
|
105
|
+
if (afterIndex >= 0) {
|
|
106
|
+
menuOrder.splice(afterIndex + 1, 0, id);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (beforeId) {
|
|
112
|
+
const beforeIndex = menuOrder.indexOf(beforeId);
|
|
113
|
+
if (beforeIndex >= 0) {
|
|
114
|
+
menuOrder.splice(beforeIndex, 0, id);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
menuOrder.push(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Ensures a menu record exists for the provided id. */
|
|
123
|
+
function ensureStoredMenu(menuId: string, label: string, options?: MenubarMenuOptions): StoredMenuState {
|
|
124
|
+
const id = normalizeMenuId(menuId);
|
|
125
|
+
const safeLabel = String(label || '').trim() || id;
|
|
126
|
+
let menu = menus.get(id);
|
|
127
|
+
|
|
128
|
+
if (!menu) {
|
|
129
|
+
menu = { id, label: safeLabel, items: [] };
|
|
130
|
+
menus.set(id, menu);
|
|
131
|
+
placeMenuId(id, options);
|
|
132
|
+
return menu;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
menu.label = safeLabel;
|
|
136
|
+
placeMenuId(id, options);
|
|
137
|
+
return menu;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Finds one menu item by action id. */
|
|
141
|
+
function findMenuItem(menu: StoredMenuState, actionId: string): StoredMenuItem | null {
|
|
142
|
+
const id = normalizeMenuId(actionId);
|
|
143
|
+
return menu.items.find((item) => item.action === id) || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Inserts a menu item relative to before/after anchors when provided. */
|
|
147
|
+
function insertMenuItem(menu: StoredMenuState, item: StoredMenuItem, before?: string, after?: string): void {
|
|
148
|
+
const beforeId = normalizeMenuId(before || '');
|
|
149
|
+
const afterId = normalizeMenuId(after || '');
|
|
150
|
+
|
|
151
|
+
const insertAt = (index: number) => {
|
|
152
|
+
const existingIndex = menu.items.findIndex((entry) => entry.id === item.id || entry.action === item.action);
|
|
153
|
+
if (existingIndex >= 0) {
|
|
154
|
+
menu.items.splice(existingIndex, 1);
|
|
155
|
+
}
|
|
156
|
+
menu.items.splice(index, 0, item);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (beforeId) {
|
|
160
|
+
const beforeIndex = menu.items.findIndex((entry) => entry.action === beforeId);
|
|
161
|
+
if (beforeIndex >= 0) {
|
|
162
|
+
insertAt(beforeIndex);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (afterId) {
|
|
168
|
+
const afterIndex = menu.items.findIndex((entry) => entry.action === afterId);
|
|
169
|
+
if (afterIndex >= 0) {
|
|
170
|
+
insertAt(afterIndex + 1);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
insertAt(menu.items.length);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Converts one stored menu item into a serializable plugin payload element. */
|
|
179
|
+
function serializeMenuItem(item: StoredMenuItem): SerializedMenuItem | null {
|
|
180
|
+
if (item.hidden) return null;
|
|
181
|
+
|
|
182
|
+
if (item.kind === 'separator') {
|
|
183
|
+
return {
|
|
184
|
+
type: 'text',
|
|
185
|
+
id: item.id,
|
|
186
|
+
content: item.content || '—',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (item.kind === 'text') {
|
|
191
|
+
return {
|
|
192
|
+
type: 'text',
|
|
193
|
+
id: item.id,
|
|
194
|
+
content: item.content || item.label,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
type: 'button',
|
|
200
|
+
id: item.action || item.id,
|
|
201
|
+
label: item.label,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Serializes the current menu registry for the host runtime. */
|
|
206
|
+
function serializeMenus(): SerializedMenuDefinition[] {
|
|
207
|
+
return menuOrder
|
|
208
|
+
.map((menuId, index) => {
|
|
209
|
+
const menu = menus.get(menuId);
|
|
210
|
+
if (!menu || menu.hidden) return null;
|
|
211
|
+
return {
|
|
212
|
+
id: menu.id,
|
|
213
|
+
label: menu.label,
|
|
214
|
+
order: index + 1,
|
|
215
|
+
elements: menu.items
|
|
216
|
+
.map((item) => serializeMenuItem(item))
|
|
217
|
+
.filter((item): item is SerializedMenuItem => Boolean(item)),
|
|
218
|
+
};
|
|
219
|
+
})
|
|
220
|
+
.filter((menu): menu is SerializedMenuDefinition => Boolean(menu));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Runs one registered action handler by id. */
|
|
224
|
+
export async function runRegisteredAction(actionId: string, ...args: unknown[]): Promise<boolean> {
|
|
225
|
+
const id = String(actionId || '').trim();
|
|
226
|
+
if (!id) return false;
|
|
227
|
+
|
|
228
|
+
const handler = actionHandlers.get(id);
|
|
229
|
+
if (!handler) return false;
|
|
230
|
+
|
|
231
|
+
await handler(...args);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
30
235
|
export interface MenuHandle {
|
|
31
236
|
id: string;
|
|
32
237
|
addItem(item: MenubarItem): void;
|
|
@@ -36,59 +241,157 @@ export interface MenuHandle {
|
|
|
36
241
|
showItem(actionId: string): void;
|
|
37
242
|
}
|
|
38
243
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
244
|
+
/** Creates a stable, mutation-based handle for one menu id. */
|
|
245
|
+
function createMenuHandle(menuId: string): MenuHandle {
|
|
246
|
+
const id = normalizeMenuId(menuId);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
id,
|
|
250
|
+
addItem(item: MenubarItem) {
|
|
251
|
+
const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
|
|
252
|
+
const label = String(item?.label || '').trim();
|
|
253
|
+
const action = String(item?.action || '').trim();
|
|
254
|
+
if (!label || !action) return;
|
|
255
|
+
|
|
256
|
+
const entry: StoredMenuItem = {
|
|
257
|
+
kind: 'button',
|
|
258
|
+
id: action,
|
|
259
|
+
label,
|
|
260
|
+
title: item.title,
|
|
261
|
+
action,
|
|
262
|
+
};
|
|
263
|
+
insertMenuItem(menu, entry, item.before, item.after);
|
|
264
|
+
|
|
265
|
+
const openvcs = getOpenVCS();
|
|
266
|
+
if (!openvcs?.menus) return;
|
|
267
|
+
openvcs.menus.getOrCreate(menu.id, menu.label);
|
|
268
|
+
openvcs.menus.addMenuItem(menu.id, item);
|
|
269
|
+
},
|
|
270
|
+
addSeparator(beforeAction?: string) {
|
|
271
|
+
const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
|
|
272
|
+
const entry: StoredMenuItem = {
|
|
273
|
+
kind: 'separator',
|
|
274
|
+
id: allocateSyntheticId(`${menu.id}-separator`),
|
|
275
|
+
label: 'Separator',
|
|
276
|
+
content: '—',
|
|
277
|
+
};
|
|
278
|
+
insertMenuItem(menu, entry, beforeAction);
|
|
279
|
+
|
|
280
|
+
const openvcs = getOpenVCS();
|
|
281
|
+
if (!openvcs?.menus) return;
|
|
282
|
+
openvcs.menus.getOrCreate(menu.id, menu.label);
|
|
283
|
+
openvcs.menus.addMenuSeparator(menu.id, beforeAction);
|
|
284
|
+
},
|
|
285
|
+
removeItem(actionId: string) {
|
|
286
|
+
const menu = getStoredMenu(this.id);
|
|
287
|
+
if (!menu) return;
|
|
288
|
+
const idToRemove = normalizeMenuId(actionId);
|
|
289
|
+
menu.items = menu.items.filter((item) => item.action !== idToRemove);
|
|
290
|
+
|
|
291
|
+
const openvcs = getOpenVCS();
|
|
292
|
+
if (openvcs?.menus) {
|
|
293
|
+
const handle = openvcs.menus.get(this.id) as MenuHandle | null;
|
|
294
|
+
handle?.removeItem(idToRemove);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
hideItem(actionId: string) {
|
|
298
|
+
const menu = getStoredMenu(this.id);
|
|
299
|
+
if (!menu) return;
|
|
300
|
+
const item = findMenuItem(menu, actionId);
|
|
301
|
+
if (item) item.hidden = true;
|
|
302
|
+
|
|
303
|
+
const openvcs = getOpenVCS();
|
|
304
|
+
if (!openvcs?.menus) return;
|
|
305
|
+
const handle = openvcs.menus.get(this.id) as MenuHandle | null;
|
|
306
|
+
handle?.hideItem(actionId);
|
|
307
|
+
},
|
|
308
|
+
showItem(actionId: string) {
|
|
309
|
+
const menu = getStoredMenu(this.id);
|
|
310
|
+
if (!menu) return;
|
|
311
|
+
const item = findMenuItem(menu, actionId);
|
|
312
|
+
if (item) item.hidden = false;
|
|
313
|
+
|
|
314
|
+
const openvcs = getOpenVCS();
|
|
315
|
+
if (!openvcs?.menus) return;
|
|
316
|
+
const handle = openvcs.menus.get(this.id) as MenuHandle | null;
|
|
317
|
+
handle?.showItem(actionId);
|
|
318
|
+
},
|
|
319
|
+
};
|
|
42
320
|
}
|
|
43
321
|
|
|
322
|
+
/** Returns a menu by id, or null when it does not exist. */
|
|
44
323
|
export function getMenu(menuId: string): MenuHandle | null {
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
47
|
-
return
|
|
324
|
+
const stored = getStoredMenu(menuId);
|
|
325
|
+
if (!stored) return null;
|
|
326
|
+
return createMenuHandle(stored.id);
|
|
48
327
|
}
|
|
49
328
|
|
|
329
|
+
/** Returns a menu by id, creating it when necessary. */
|
|
50
330
|
export function getOrCreateMenu(menuId: string, label: string): MenuHandle | null {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
return castMenuHandle(openvcs.menus.getOrCreate(menuId, label));
|
|
331
|
+
const stored = ensureStoredMenu(menuId, label);
|
|
332
|
+
return createMenuHandle(stored.id);
|
|
54
333
|
}
|
|
55
334
|
|
|
335
|
+
/** Creates a menu at a specific insertion point. */
|
|
56
336
|
export function createMenu(menuId: string, label: string, options?: MenubarMenuOptions): MenuHandle | null {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
return castMenuHandle(openvcs.menus.create(menuId, label, options));
|
|
337
|
+
const stored = ensureStoredMenu(menuId, label, options);
|
|
338
|
+
return createMenuHandle(stored.id);
|
|
60
339
|
}
|
|
61
340
|
|
|
341
|
+
/** Adds one action item to a menu. */
|
|
62
342
|
export function addMenuItem(menuId: string, item: MenubarItem): void {
|
|
63
|
-
const
|
|
64
|
-
|
|
343
|
+
const menu = getStoredMenu(menuId) || ensureStoredMenu(menuId, menuId);
|
|
344
|
+
const handle = createMenuHandle(menu.id);
|
|
345
|
+
handle.addItem(item);
|
|
65
346
|
}
|
|
66
347
|
|
|
348
|
+
/** Adds one separator item to a menu. */
|
|
67
349
|
export function addMenuSeparator(menuId: string, beforeAction?: string): void {
|
|
68
|
-
const
|
|
69
|
-
|
|
350
|
+
const menu = getStoredMenu(menuId) || ensureStoredMenu(menuId, menuId);
|
|
351
|
+
const handle = createMenuHandle(menu.id);
|
|
352
|
+
handle.addSeparator(beforeAction);
|
|
70
353
|
}
|
|
71
354
|
|
|
355
|
+
/** Removes one menu entirely. */
|
|
72
356
|
export function removeMenu(menuId: string): void {
|
|
357
|
+
const id = normalizeMenuId(menuId);
|
|
358
|
+
menus.delete(id);
|
|
359
|
+
const index = menuOrder.indexOf(id);
|
|
360
|
+
if (index >= 0) menuOrder.splice(index, 1);
|
|
361
|
+
|
|
73
362
|
const openvcs = getOpenVCS();
|
|
74
|
-
openvcs?.menus?.remove(
|
|
363
|
+
openvcs?.menus?.remove(id);
|
|
75
364
|
}
|
|
76
365
|
|
|
366
|
+
/** Hides one menu from the host UI. */
|
|
77
367
|
export function hideMenu(menuId: string): void {
|
|
368
|
+
const menu = getStoredMenu(menuId);
|
|
369
|
+
if (menu) menu.hidden = true;
|
|
370
|
+
|
|
78
371
|
const openvcs = getOpenVCS();
|
|
79
|
-
openvcs?.menus?.hide(menuId);
|
|
372
|
+
openvcs?.menus?.hide(normalizeMenuId(menuId));
|
|
80
373
|
}
|
|
81
374
|
|
|
375
|
+
/** Shows one previously hidden menu. */
|
|
82
376
|
export function showMenu(menuId: string): void {
|
|
377
|
+
const menu = getStoredMenu(menuId);
|
|
378
|
+
if (menu) menu.hidden = false;
|
|
379
|
+
|
|
83
380
|
const openvcs = getOpenVCS();
|
|
84
|
-
openvcs?.menus?.show(menuId);
|
|
381
|
+
openvcs?.menus?.show(normalizeMenuId(menuId));
|
|
85
382
|
}
|
|
86
383
|
|
|
384
|
+
/** Registers an action handler by id. */
|
|
87
385
|
export function registerAction(id: string, handler: (...args: unknown[]) => unknown): void {
|
|
386
|
+
const key = String(id || '').trim();
|
|
387
|
+
if (!key) return;
|
|
388
|
+
actionHandlers.set(key, handler);
|
|
389
|
+
|
|
88
390
|
const openvcs = getOpenVCS();
|
|
89
|
-
openvcs?.registerAction(
|
|
391
|
+
openvcs?.registerAction(key, handler);
|
|
90
392
|
}
|
|
91
393
|
|
|
394
|
+
/** Invokes a host command when the host exposes a direct invoke helper. */
|
|
92
395
|
export function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T> {
|
|
93
396
|
const openvcs = getOpenVCS();
|
|
94
397
|
if (!openvcs) {
|
|
@@ -97,7 +400,23 @@ export function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T> {
|
|
|
97
400
|
return openvcs.invoke(cmd, args);
|
|
98
401
|
}
|
|
99
402
|
|
|
403
|
+
/** Emits a user notification when the host exposes one. */
|
|
100
404
|
export function notify(msg: string): void {
|
|
101
405
|
const openvcs = getOpenVCS();
|
|
102
406
|
openvcs?.notify(msg);
|
|
103
407
|
}
|
|
408
|
+
|
|
409
|
+
/** Builds the SDK plugin delegates contributed by the menu registry. */
|
|
410
|
+
export function createMenuPluginDelegates(): PluginDelegates<PluginRuntimeContext> {
|
|
411
|
+
return {
|
|
412
|
+
async 'plugin.get_menus'(): Promise<PluginMenuDefinition[]> {
|
|
413
|
+
return serializeMenus() as unknown as PluginMenuDefinition[];
|
|
414
|
+
},
|
|
415
|
+
async 'plugin.handle_action'(params: PluginHandleActionParams): Promise<null> {
|
|
416
|
+
const actionId = String(params?.action_id || '').trim();
|
|
417
|
+
if (!actionId) return null;
|
|
418
|
+
await runRegisteredAction(actionId);
|
|
419
|
+
return null;
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
@@ -14,6 +14,10 @@ import type {
|
|
|
14
14
|
PluginRuntimeTransport,
|
|
15
15
|
} from './contracts';
|
|
16
16
|
import { createPluginRuntime } from './factory';
|
|
17
|
+
import {
|
|
18
|
+
createMenuPluginDelegates,
|
|
19
|
+
runRegisteredAction,
|
|
20
|
+
} from './menu';
|
|
17
21
|
|
|
18
22
|
/** Describes the plugin module startup hook invoked by the generated bootstrap. */
|
|
19
23
|
export type OnPluginStartHandler = () => void | Promise<void>;
|
|
@@ -56,8 +60,35 @@ export interface BootstrapPluginModuleOptions {
|
|
|
56
60
|
export function createRegisteredPluginRuntime(
|
|
57
61
|
definition: PluginModuleDefinition = {},
|
|
58
62
|
): PluginRuntime {
|
|
63
|
+
const menuDelegates = createMenuPluginDelegates();
|
|
64
|
+
const explicitPluginDelegates = definition.plugin ?? {};
|
|
65
|
+
const explicitGetMenus = explicitPluginDelegates['plugin.get_menus'];
|
|
66
|
+
const explicitHandleAction = explicitPluginDelegates['plugin.handle_action'];
|
|
67
|
+
|
|
59
68
|
const options: CreatePluginRuntimeOptions = {
|
|
60
|
-
plugin:
|
|
69
|
+
plugin: {
|
|
70
|
+
...explicitPluginDelegates,
|
|
71
|
+
'plugin.get_menus': async (params, ctx) => {
|
|
72
|
+
const menus = await menuDelegates['plugin.get_menus']?.(params, ctx);
|
|
73
|
+
const explicitMenus = explicitGetMenus
|
|
74
|
+
? await explicitGetMenus(params, ctx)
|
|
75
|
+
: [];
|
|
76
|
+
return [
|
|
77
|
+
...(Array.isArray(explicitMenus) ? explicitMenus : []),
|
|
78
|
+
...(Array.isArray(menus) ? menus : []),
|
|
79
|
+
];
|
|
80
|
+
},
|
|
81
|
+
'plugin.handle_action': async (params, ctx) => {
|
|
82
|
+
const actionId = String(params?.action_id || '').trim();
|
|
83
|
+
if (actionId && (await runRegisteredAction(actionId))) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (explicitHandleAction) {
|
|
87
|
+
return explicitHandleAction(params, ctx);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
},
|
|
91
|
+
},
|
|
61
92
|
vcs: definition.vcs,
|
|
62
93
|
implements: definition.implements,
|
|
63
94
|
logTarget: definition.logTarget,
|