@qping/plugin-bus 0.2.0 → 0.4.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.
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Action registry types.
3
+ *
4
+ * A plugin registers its full action set once at startup via `plugin.actions([...])`. Search
5
+ * items and the detail page only reference actions by id; the parameters an action needs are
6
+ * produced inside its own `execute`, never carried on the item.
7
+ */
8
+ import type { PluginHostEnv } from "./hostEnv.ts";
9
+ /** An i18n message key plus the English fallback the host uses when the key is missing. */
10
+ export type LocalizedText = {
11
+ key: string;
12
+ defaultValue: string;
13
+ };
14
+ /** Keys the host deliberately permits for action shortcuts. */
15
+ export declare const Key: {
16
+ readonly Enter: "Enter";
17
+ readonly Tab: "Tab";
18
+ readonly Space: "Space";
19
+ readonly Delete: "Delete";
20
+ readonly Backspace: "Backspace";
21
+ readonly Escape: "Escape";
22
+ readonly Left: "Left";
23
+ readonly Right: "Right";
24
+ readonly Up: "Up";
25
+ readonly Down: "Down";
26
+ readonly A: "A";
27
+ readonly B: "B";
28
+ readonly C: "C";
29
+ readonly D: "D";
30
+ readonly E: "E";
31
+ readonly F: "F";
32
+ readonly G: "G";
33
+ readonly H: "H";
34
+ readonly I: "I";
35
+ readonly J: "J";
36
+ readonly K: "K";
37
+ readonly L: "L";
38
+ readonly M: "M";
39
+ readonly N: "N";
40
+ readonly O: "O";
41
+ readonly P: "P";
42
+ readonly Q: "Q";
43
+ readonly R: "R";
44
+ readonly S: "S";
45
+ readonly T: "T";
46
+ readonly U: "U";
47
+ readonly V: "V";
48
+ readonly W: "W";
49
+ readonly X: "X";
50
+ readonly Y: "Y";
51
+ readonly Z: "Z";
52
+ readonly D0: "D0";
53
+ readonly D1: "D1";
54
+ readonly D2: "D2";
55
+ readonly D3: "D3";
56
+ readonly D4: "D4";
57
+ readonly D5: "D5";
58
+ readonly D6: "D6";
59
+ readonly D7: "D7";
60
+ readonly D8: "D8";
61
+ readonly D9: "D9";
62
+ readonly F1: "F1";
63
+ readonly F2: "F2";
64
+ readonly F3: "F3";
65
+ readonly F4: "F4";
66
+ readonly F5: "F5";
67
+ readonly F6: "F6";
68
+ readonly F7: "F7";
69
+ readonly F8: "F8";
70
+ readonly F9: "F9";
71
+ readonly F10: "F10";
72
+ readonly F11: "F11";
73
+ readonly F12: "F12";
74
+ };
75
+ export type HotkeyKey = (typeof Key)[keyof typeof Key];
76
+ /** Permitted modifier combinations, e.g. `Modifiers.ControlShift`. */
77
+ export declare const Modifiers: {
78
+ readonly None: 0;
79
+ readonly Control: 1;
80
+ readonly Alt: 2;
81
+ readonly ControlAlt: 3;
82
+ readonly Shift: 4;
83
+ readonly ControlShift: 5;
84
+ readonly AltShift: 6;
85
+ readonly ControlAltShift: 7;
86
+ };
87
+ export type HotkeyModifiers = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
88
+ export type Hotkey = {
89
+ key: HotkeyKey;
90
+ modifiers?: HotkeyModifiers;
91
+ };
92
+ /** Actions the host itself can carry out. Anything else belongs in `execute`. */
93
+ export declare const HostAction: {
94
+ readonly Copy: "copy";
95
+ readonly CopyAndPaste: "copyAndPaste";
96
+ readonly AddClipboardHistory: "addClipboardHistory";
97
+ readonly Execute: "execute";
98
+ readonly OpenInExplorer: "openInExplorer";
99
+ readonly OpenInBrowser: "openInBrowser";
100
+ readonly OpenPlugin: "openPlugin";
101
+ readonly Run: "run";
102
+ readonly Kill: "kill";
103
+ };
104
+ export type HostActionKind = (typeof HostAction)[keyof typeof HostAction];
105
+ /** Command spec for {@link HostAction.Run}. */
106
+ export type RunSpec = {
107
+ name?: string;
108
+ command: string;
109
+ args?: string;
110
+ workingDirectory?: string;
111
+ runAsAdmin?: boolean;
112
+ isBashScript?: boolean;
113
+ scripts?: string | string[];
114
+ };
115
+ /**
116
+ * What the host should do, with the parameters that kind actually needs. The discriminated union
117
+ * is the point: `{ kind: HostAction.Copy, path }` does not compile.
118
+ */
119
+ export type HostActionRequest = {
120
+ kind: typeof HostAction.Copy;
121
+ text: string;
122
+ } | {
123
+ kind: typeof HostAction.CopyAndPaste;
124
+ text: string;
125
+ } | {
126
+ kind: typeof HostAction.AddClipboardHistory;
127
+ texts: string[];
128
+ } | {
129
+ kind: typeof HostAction.Execute;
130
+ path: string;
131
+ args?: string;
132
+ runAsAdmin?: boolean;
133
+ } | {
134
+ kind: typeof HostAction.OpenInExplorer;
135
+ path: string;
136
+ } | {
137
+ kind: typeof HostAction.OpenInBrowser;
138
+ url: string | string[];
139
+ } | {
140
+ kind: typeof HostAction.OpenPlugin;
141
+ pluginId: string;
142
+ } | {
143
+ kind: typeof HostAction.Run;
144
+ command: RunSpec;
145
+ } | {
146
+ kind: typeof HostAction.Kill;
147
+ pid: number;
148
+ };
149
+ /** Opens a web detail page. `page` defaults to the entry declared in plugin.json. */
150
+ export type DetailRequest = {
151
+ page?: string;
152
+ title?: string;
153
+ initialState?: unknown;
154
+ };
155
+ export type ActionTarget =
156
+ /** Run one privileged host-side action (clipboard, process launch, browser, ...). */
157
+ {
158
+ kind: "host";
159
+ action: HostActionRequest;
160
+ }
161
+ /** Hand off to the currently active detail page as host.event.detailAction. */
162
+ | {
163
+ kind: "web";
164
+ payload?: unknown;
165
+ }
166
+ /** Open a new web detail page. */
167
+ | ({
168
+ kind: "detail";
169
+ } & DetailRequest);
170
+ export type ActionAfter = "keep" | "close" | "refresh";
171
+ type ActionOutcomeBase = {
172
+ /** Status bar text. */
173
+ message?: LocalizedText;
174
+ };
175
+ /**
176
+ * The result of running an action. An action selects at most one execution target. Host targets
177
+ * may choose a follow-up lifecycle action; web and detail targets keep their current surface alive.
178
+ */
179
+ export type ActionOutcome = ActionOutcomeBase & ({
180
+ target?: undefined;
181
+ after?: ActionAfter;
182
+ } | {
183
+ target: Extract<ActionTarget, {
184
+ kind: "host";
185
+ }>;
186
+ after?: ActionAfter;
187
+ } | {
188
+ target: Exclude<ActionTarget, {
189
+ kind: "host";
190
+ }>;
191
+ after?: "keep";
192
+ });
193
+ /**
194
+ * What an action sees when it runs. `item` is the original object returned by `search()`,
195
+ * including fields the host never saw — the SDK keeps it so actions do not have to re-derive
196
+ * their data from the item id.
197
+ */
198
+ export type ActionContext<TItem = unknown> = PluginHostEnv & {
199
+ actionId: string;
200
+ itemId: string;
201
+ query: string;
202
+ item?: TItem;
203
+ };
204
+ /**
205
+ * One registered action. `hotkey` is optional: without it the first registered action gets Enter
206
+ * and the rest are click-only, matching search result items.
207
+ */
208
+ export type ActionDefinition<TItem = any> = {
209
+ id: string;
210
+ title: LocalizedText;
211
+ description?: LocalizedText;
212
+ hotkey?: Hotkey;
213
+ execute: (context: ActionContext<TItem>) => ActionOutcome | void | Promise<ActionOutcome | void>;
214
+ };
215
+ /** The registry shape sent to the host in the initialize response (no `execute`). */
216
+ export type ActionManifestEntry = {
217
+ id: string;
218
+ title: LocalizedText;
219
+ description?: LocalizedText;
220
+ hotkey?: Hotkey;
221
+ };
222
+ export declare function toActionManifest(definition: ActionDefinition): ActionManifestEntry;
223
+ export {};
@@ -10,7 +10,9 @@
10
10
  import { NodeTransport } from "./transport.ts";
11
11
  import { HandlerRouter } from "./router.ts";
12
12
  export interface PluginHandlers {
13
- [route: string]: (payload: any) => Promise<any> | any;
13
+ [route: string]: (payload: any, context: {
14
+ sessionId: string;
15
+ }) => Promise<any> | any;
14
16
  }
15
17
  export interface PluginRuntime {
16
18
  transport: NodeTransport;
@@ -167,6 +167,7 @@ var Routes = {
167
167
  Initialize: "host.event.initialize",
168
168
  Search: "host.event.search",
169
169
  Key: "host.event.key",
170
+ DetailAction: "host.event.detailAction",
170
171
  LanguageChanged: "host.event.languageChanged",
171
172
  ThemeChanged: "host.event.themeChanged",
172
173
  InputActionCaptured: "host.event.inputActionCaptured"
@@ -330,7 +331,10 @@ var HandlerRouter = class {
330
331
  }
331
332
  const deadlineMs = deadlineFromTimeoutMs(env.timeoutMs);
332
333
  try {
333
- const result = await requestScope.run({ deadlineMs }, () => handler(env.payload));
334
+ const result = await requestScope.run(
335
+ { deadlineMs },
336
+ () => handler(env.payload, { sessionId: env.sessionId })
337
+ );
334
338
  this.send(this.responseFor(env, result ?? {}));
335
339
  } catch (err) {
336
340
  const message = err instanceof Error ? err.message : String(err);
package/dist/dev.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Notifies a running MyTools instance that a development plugin was rebuilt.
3
+ * The request is retried once after a short delay when the pipe is not yet available.
4
+ */
5
+ export declare function requestDevelopmentPluginRefresh(pluginId: string): Promise<void>;
package/dist/dev.mjs ADDED
@@ -0,0 +1,80 @@
1
+ // src/developmentRefresh.ts
2
+ import { createConnection } from "node:net";
3
+ var DEVELOPMENT_REFRESH_PIPE_PATH = "\\\\.\\pipe\\MyTools.DevelopmentPlugins.Refresh";
4
+ var DEFAULT_RETRY_DELAY_MS = 250;
5
+ var DEFAULT_MAX_ATTEMPTS = 2;
6
+ var DEFAULT_REQUEST_TIMEOUT_MS = 2e3;
7
+ var VALID_PLUGIN_ID = /^[a-z0-9](?:[a-z0-9.-]{0,62}[a-z0-9])?$/;
8
+ async function requestDevelopmentPluginRefreshWithOptions(pluginId, options = {}) {
9
+ if (!VALID_PLUGIN_ID.test(pluginId)) {
10
+ throw new TypeError(`Invalid MyTools plugin ID: ${pluginId}`);
11
+ }
12
+ const pipePath = options.pipePath ?? DEVELOPMENT_REFRESH_PIPE_PATH;
13
+ const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
14
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
15
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
16
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
17
+ throw new RangeError("maxAttempts must be a positive integer");
18
+ }
19
+ if (!Number.isFinite(retryDelayMs) || retryDelayMs < 0) {
20
+ throw new RangeError("retryDelayMs must be a non-negative number");
21
+ }
22
+ if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) {
23
+ throw new RangeError("requestTimeoutMs must be a positive number");
24
+ }
25
+ let lastError;
26
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
27
+ try {
28
+ await sendRefreshRequest(pipePath, pluginId, requestTimeoutMs);
29
+ return;
30
+ } catch (error) {
31
+ lastError = error;
32
+ if (attempt < maxAttempts) {
33
+ await delay(retryDelayMs);
34
+ }
35
+ }
36
+ }
37
+ throw new Error(
38
+ `Failed to request MyTools refresh for ${pluginId} after ${maxAttempts} attempts`,
39
+ { cause: lastError }
40
+ );
41
+ }
42
+ function sendRefreshRequest(pipePath, pluginId, requestTimeoutMs) {
43
+ return new Promise((resolve, reject) => {
44
+ const socket = createConnection(pipePath);
45
+ let settled = false;
46
+ let timeout;
47
+ const settle = (error) => {
48
+ if (settled) return;
49
+ settled = true;
50
+ if (timeout) clearTimeout(timeout);
51
+ if (error) reject(error);
52
+ else resolve();
53
+ };
54
+ socket.once("connect", () => {
55
+ socket.end(`${pluginId}
56
+ `, () => {
57
+ settle();
58
+ });
59
+ });
60
+ socket.once("error", (error) => {
61
+ socket.destroy();
62
+ settle(error);
63
+ });
64
+ timeout = setTimeout(() => {
65
+ socket.destroy();
66
+ settle(new Error(`Development refresh request timed out after ${requestTimeoutMs}ms`));
67
+ }, requestTimeoutMs);
68
+ });
69
+ }
70
+ function delay(milliseconds) {
71
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
72
+ }
73
+
74
+ // src/dev.ts
75
+ function requestDevelopmentPluginRefresh(pluginId) {
76
+ return requestDevelopmentPluginRefreshWithOptions(pluginId);
77
+ }
78
+ export {
79
+ requestDevelopmentPluginRefresh
80
+ };
@@ -0,0 +1,9 @@
1
+ export declare const DEVELOPMENT_REFRESH_PIPE_PATH = "\\\\.\\pipe\\MyTools.DevelopmentPlugins.Refresh";
2
+ export type DevelopmentRefreshRequestOptions = {
3
+ pipePath?: string;
4
+ retryDelayMs?: number;
5
+ maxAttempts?: number;
6
+ requestTimeoutMs?: number;
7
+ };
8
+ /** Internal implementation with injectable timing and endpoint for protocol tests. */
9
+ export declare function requestDevelopmentPluginRefreshWithOptions(pluginId: string, options?: DevelopmentRefreshRequestOptions): Promise<void>;
@@ -0,0 +1,9 @@
1
+ /** Environment the host stamps onto every plugin.call payload. */
2
+ export type PluginTheme = "light" | "dark";
3
+ export type PluginHostEnv = {
4
+ locale: string;
5
+ fallbackLocale: string;
6
+ theme: PluginTheme;
7
+ };
8
+ export declare function asTheme(value: unknown): PluginTheme;
9
+ export declare function asHostEnv(payload: any): PluginHostEnv;
@@ -0,0 +1,2 @@
1
+ /** Render a host-formatted shortcut as the same sequence of keycaps used by the desktop action bar. */
2
+ export declare function renderHotkeyKeycaps(element: HTMLElement, hotkey: string): void;
package/dist/node.d.ts CHANGED
@@ -4,17 +4,15 @@
4
4
  * Method names map to v3 routes:
5
5
  * initialize -> plugin.call.initialize
6
6
  * search -> plugin.call.search
7
- * action -> plugin.call.invokeAction
7
+ * actions -> plugin.call.invokeAction (dispatched by action id)
8
8
  * handle(name) -> plugin.call.<name>
9
9
  * publish -> plugin.event.<subjectId>
10
10
  * hostCall -> host.call.<method>
11
11
  */
12
- export type PluginTheme = "light" | "dark";
13
- export type PluginHostEnv = {
14
- locale: string;
15
- fallbackLocale: string;
16
- theme: PluginTheme;
17
- };
12
+ import { type PluginHostEnv } from "./hostEnv.ts";
13
+ import { type ActionDefinition } from "./actions.ts";
14
+ export type { PluginHostEnv, PluginTheme } from "./hostEnv.ts";
15
+ export { HostAction, Key, Modifiers, type ActionContext, type ActionDefinition, type ActionOutcome, type DetailRequest, type HostActionKind, type HostActionRequest, type Hotkey, type HotkeyKey, type HotkeyModifiers, type LocalizedText, type RunSpec, } from "./actions.ts";
18
16
  export type PluginContext = PluginHostEnv & {
19
17
  action: string;
20
18
  itemId: string;
@@ -29,21 +27,40 @@ export type PluginSearchParams = PluginHostEnv & {
29
27
  query: string;
30
28
  mode: "global" | "plugin";
31
29
  };
32
- /** Payload of plugin.call.invokeAction. */
33
- export type PluginActionParams = PluginHostEnv & {
34
- itemId: string;
35
- actionId: string;
36
- query: string;
30
+ export type SearchIcon = {
31
+ kind: string;
32
+ value: string;
33
+ };
34
+ /**
35
+ * A search result row. Only `id`/`title`/`subtitle`/`priority`/`icon`/`actions` reach the host;
36
+ * any other field stays on the Node side and comes back as `context.item` when an action runs.
37
+ */
38
+ export type SearchItem = {
39
+ id: string;
40
+ title: string;
41
+ subtitle?: string;
42
+ priority?: number;
43
+ icon?: SearchIcon;
44
+ /** Ids of registered actions, in display order. The first one is bound to Enter. */
45
+ actions?: string[];
46
+ [extra: string]: unknown;
47
+ };
48
+ export type SearchResult = {
49
+ items: SearchItem[];
37
50
  };
38
51
  type PluginInitializeHandler = (params: PluginInitializeParams) => unknown | Promise<unknown>;
39
- type PluginSearchHandler = (params: PluginSearchParams) => unknown | Promise<unknown>;
40
- type PluginActionHandler = (params: PluginActionParams) => unknown | Promise<unknown>;
52
+ type PluginSearchHandler = (params: PluginSearchParams) => SearchResult | Promise<SearchResult>;
41
53
  type PluginHandler = (payload: any, context: PluginContext) => unknown | Promise<unknown>;
42
54
  export declare class Plugin {
43
55
  #private;
44
56
  initialize(handler: PluginInitializeHandler): this;
45
57
  search(handler: PluginSearchHandler): this;
46
- action(handler: PluginActionHandler): this;
58
+ /**
59
+ * Registers every action this plugin offers. The list is sent to the host in the initialize
60
+ * response, so the host knows the ids, labels and hotkeys before any search runs; search items
61
+ * and the detail page then reference them by id only.
62
+ */
63
+ actions<TItem = any>(definitions: ActionDefinition<TItem>[]): this;
47
64
  handle(action: string, handler: PluginHandler): this;
48
65
  /** Publishes a plugin.event.<subjectId> event to all webviews in the session. */
49
66
  publish(subjectId: string, payload?: unknown): void;
@@ -58,8 +75,9 @@ export declare class Plugin {
58
75
  * Builds the v3 route map from the fluent registrations. Exposed for unit testing the mapping
59
76
  * without connecting a pipe.
60
77
  */
61
- buildRoutes(): Record<string, (payload: any) => unknown | Promise<unknown>>;
78
+ buildRoutes(): Record<string, (payload: any, context?: {
79
+ sessionId: string;
80
+ }) => unknown | Promise<unknown>>;
62
81
  stop(): Promise<void>;
63
82
  }
64
83
  export declare function createPlugin(): Plugin;
65
- export {};