@checkstack/command-backend 0.0.2
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/CHANGELOG.md +64 -0
- package/package.json +25 -0
- package/src/index.ts +58 -0
- package/src/registry.ts +274 -0
- package/src/router.ts +105 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @checkstack/command-backend
|
|
2
|
+
|
|
3
|
+
## 0.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
|
|
8
|
+
- Updated dependencies [d20d274]
|
|
9
|
+
- @checkstack/backend-api@0.0.2
|
|
10
|
+
- @checkstack/command-common@0.0.2
|
|
11
|
+
- @checkstack/common@0.0.2
|
|
12
|
+
|
|
13
|
+
## 0.1.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- a65e002: Add command palette commands and deep-linking support
|
|
18
|
+
|
|
19
|
+
**Backend Changes:**
|
|
20
|
+
|
|
21
|
+
- `healthcheck-backend`: Add "Manage Health Checks" (⇧⌘H) and "Create Health Check" commands
|
|
22
|
+
- `catalog-backend`: Add "Manage Systems" (⇧⌘S) and "Create System" commands
|
|
23
|
+
- `integration-backend`: Add "Manage Integrations" (⇧⌘G), "Create Integration Subscription", and "View Integration Logs" commands
|
|
24
|
+
- `auth-backend`: Add "Manage Users" (⇧⌘U), "Create User", "Manage Roles", and "Manage Applications" commands
|
|
25
|
+
- `command-backend`: Auto-cleanup command registrations when plugins are deregistered
|
|
26
|
+
|
|
27
|
+
**Frontend Changes:**
|
|
28
|
+
|
|
29
|
+
- `HealthCheckConfigPage`: Handle `?action=create` URL parameter
|
|
30
|
+
- `CatalogConfigPage`: Handle `?action=create` URL parameter
|
|
31
|
+
- `IntegrationsPage`: Handle `?action=create` URL parameter
|
|
32
|
+
- `AuthSettingsPage`: Handle `?tab=` and `?action=create` URL parameters
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- a65e002: Add compile-time type safety for Lucide icon names
|
|
37
|
+
|
|
38
|
+
- Add `LucideIconName` type and `lucideIconSchema` Zod schema to `@checkstack/common`
|
|
39
|
+
- Update backend interfaces (`AuthStrategy`, `NotificationStrategy`, `IntegrationProvider`, `CommandDefinition`) to use `LucideIconName`
|
|
40
|
+
- Update RPC contracts to use `lucideIconSchema` for proper type inference across RPC boundaries
|
|
41
|
+
- Simplify `SocialProviderButton` to use `DynamicIcon` directly (removes 30+ lines of pascalCase conversion)
|
|
42
|
+
- Replace static `iconMap` in `SearchDialog` with `DynamicIcon` for dynamic icon rendering
|
|
43
|
+
- Add fallback handling in `DynamicIcon` when icon name isn't found
|
|
44
|
+
- Fix legacy kebab-case icon names to PascalCase: `mail`→`Mail`, `send`→`Send`, `github`→`Github`, `key-round`→`KeyRound`, `network`→`Network`, `AlertCircle`→`CircleAlert`
|
|
45
|
+
|
|
46
|
+
- Updated dependencies [b4eb432]
|
|
47
|
+
- Updated dependencies [a65e002]
|
|
48
|
+
- @checkstack/backend-api@1.1.0
|
|
49
|
+
- @checkstack/common@0.2.0
|
|
50
|
+
- @checkstack/command-common@0.0.3
|
|
51
|
+
|
|
52
|
+
## 0.0.2
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- Updated dependencies [ffc28f6]
|
|
57
|
+
- Updated dependencies [71275dd]
|
|
58
|
+
- Updated dependencies [ae19ff6]
|
|
59
|
+
- Updated dependencies [b55fae6]
|
|
60
|
+
- Updated dependencies [b354ab3]
|
|
61
|
+
- Updated dependencies [81f3f85]
|
|
62
|
+
- @checkstack/common@0.1.0
|
|
63
|
+
- @checkstack/backend-api@1.0.0
|
|
64
|
+
- @checkstack/command-common@0.0.2
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/command-backend",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"lint": "bun run lint:code",
|
|
9
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@checkstack/backend-api": "workspace:*",
|
|
13
|
+
"@checkstack/command-common": "workspace:*",
|
|
14
|
+
"@checkstack/common": "workspace:*",
|
|
15
|
+
"@orpc/server": "^1.13.2",
|
|
16
|
+
"zod": "^4.2.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@checkstack/scripts": "workspace:*",
|
|
20
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
21
|
+
"@types/bun": "^1.3.5",
|
|
22
|
+
"@types/node": "^20.0.0",
|
|
23
|
+
"typescript": "^5.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBackendPlugin,
|
|
3
|
+
coreServices,
|
|
4
|
+
coreHooks,
|
|
5
|
+
} from "@checkstack/backend-api";
|
|
6
|
+
import {
|
|
7
|
+
pluginMetadata,
|
|
8
|
+
commandContract,
|
|
9
|
+
} from "@checkstack/command-common";
|
|
10
|
+
import { createCommandRouter } from "./router";
|
|
11
|
+
import { unregisterProvidersByPluginId } from "./registry";
|
|
12
|
+
|
|
13
|
+
// Re-export registry functions for other plugins to use
|
|
14
|
+
export {
|
|
15
|
+
registerSearchProvider,
|
|
16
|
+
unregisterSearchProvider,
|
|
17
|
+
clearRegistry,
|
|
18
|
+
type BackendSearchProvider,
|
|
19
|
+
type SearchContext,
|
|
20
|
+
type CommandDefinition,
|
|
21
|
+
type RegisterSearchProviderOptions,
|
|
22
|
+
} from "./registry";
|
|
23
|
+
|
|
24
|
+
export default createBackendPlugin({
|
|
25
|
+
metadata: pluginMetadata,
|
|
26
|
+
register(env) {
|
|
27
|
+
env.registerInit({
|
|
28
|
+
deps: {
|
|
29
|
+
rpc: coreServices.rpc,
|
|
30
|
+
logger: coreServices.logger,
|
|
31
|
+
},
|
|
32
|
+
init: async ({ rpc, logger }) => {
|
|
33
|
+
logger.debug("Initializing Command Backend...");
|
|
34
|
+
|
|
35
|
+
// Register oRPC router
|
|
36
|
+
const commandRouter = createCommandRouter();
|
|
37
|
+
rpc.registerRouter(commandRouter, commandContract);
|
|
38
|
+
|
|
39
|
+
logger.debug("✅ Command Backend initialized.");
|
|
40
|
+
},
|
|
41
|
+
afterPluginsReady: async ({ logger, onHook }) => {
|
|
42
|
+
// Subscribe to plugin deregistration to clean up their commands
|
|
43
|
+
onHook(
|
|
44
|
+
coreHooks.pluginDeregistering,
|
|
45
|
+
async ({ pluginId }) => {
|
|
46
|
+
const removed = unregisterProvidersByPluginId(pluginId);
|
|
47
|
+
if (removed > 0) {
|
|
48
|
+
logger.debug(
|
|
49
|
+
`[command-backend] Unregistered ${removed} search provider(s) for plugin: ${pluginId}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{ mode: "instance-local" }
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
});
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { SearchResult } from "@checkstack/command-common";
|
|
2
|
+
import {
|
|
3
|
+
qualifyPermissionId,
|
|
4
|
+
type PluginMetadata,
|
|
5
|
+
type Permission,
|
|
6
|
+
type LucideIconName,
|
|
7
|
+
} from "@checkstack/common";
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// TYPES
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Context provided to search providers during search operations.
|
|
15
|
+
*/
|
|
16
|
+
export interface SearchContext {
|
|
17
|
+
/** The user's permission IDs for filtering results */
|
|
18
|
+
userPermissions: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A command definition for simple registration.
|
|
23
|
+
* Permissions will be automatically qualified using the plugin's metadata.
|
|
24
|
+
*/
|
|
25
|
+
export interface CommandDefinition {
|
|
26
|
+
/** Unique command ID (will be prefixed with plugin ID) */
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
subtitle?: string;
|
|
30
|
+
/** Icon name (Lucide PascalCase, e.g., 'AlertCircle') */
|
|
31
|
+
iconName?: LucideIconName;
|
|
32
|
+
/** Keyboard shortcuts, e.g. ["meta+shift+i", "ctrl+shift+i"] */
|
|
33
|
+
shortcuts?: string[];
|
|
34
|
+
/** Route to navigate to when executed */
|
|
35
|
+
route: string;
|
|
36
|
+
/** Permissions required (will be auto-qualified with plugin ID) */
|
|
37
|
+
requiredPermissions?: Permission[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A backend search provider that contributes results to the command palette.
|
|
42
|
+
*/
|
|
43
|
+
export interface BackendSearchProvider {
|
|
44
|
+
/** Unique identifier for this provider */
|
|
45
|
+
id: string;
|
|
46
|
+
/** Display name for the provider category */
|
|
47
|
+
name: string;
|
|
48
|
+
/** Higher priority = appears first in results (default: 0) */
|
|
49
|
+
priority?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Search function that returns results matching the query.
|
|
52
|
+
* Results should NOT be pre-filtered by permissions - the registry handles that.
|
|
53
|
+
*/
|
|
54
|
+
search: (
|
|
55
|
+
query: string,
|
|
56
|
+
context: SearchContext
|
|
57
|
+
) => Promise<SearchResult[]> | SearchResult[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Options for registering a search provider.
|
|
62
|
+
*/
|
|
63
|
+
export interface RegisterSearchProviderOptions {
|
|
64
|
+
/** The plugin's metadata - used to qualify permission IDs */
|
|
65
|
+
pluginMetadata: PluginMetadata;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Simple command definitions. These will be automatically:
|
|
69
|
+
* - Converted to a search provider
|
|
70
|
+
* - Have permissions qualified with pluginId
|
|
71
|
+
* - Be searchable by title, subtitle, and category
|
|
72
|
+
*/
|
|
73
|
+
commands?: CommandDefinition[];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Custom search provider for more complex search logic.
|
|
77
|
+
* Use this for entity search (e.g., catalog systems).
|
|
78
|
+
* Permissions in returned results will be auto-qualified.
|
|
79
|
+
*/
|
|
80
|
+
provider?: Omit<BackendSearchProvider, "id"> & {
|
|
81
|
+
/** Provider ID (will be prefixed with plugin ID) */
|
|
82
|
+
id: string;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// INTERNAL REGISTRY
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
const searchProviders = new Map<string, BackendSearchProvider>();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Register a search provider with the command palette.
|
|
94
|
+
*
|
|
95
|
+
* This is the main API for plugins to contribute to command palette search.
|
|
96
|
+
* Permissions are automatically qualified with the plugin's ID.
|
|
97
|
+
*
|
|
98
|
+
* @example Simple command registration:
|
|
99
|
+
* ```ts
|
|
100
|
+
* registerSearchProvider({
|
|
101
|
+
* pluginMetadata,
|
|
102
|
+
* commands: [
|
|
103
|
+
* {
|
|
104
|
+
* id: "create",
|
|
105
|
+
* title: "Create Incident",
|
|
106
|
+
* subtitle: "Report a new incident",
|
|
107
|
+
* iconName: "AlertCircle",
|
|
108
|
+
* shortcuts: ["meta+shift+i", "ctrl+shift+i"],
|
|
109
|
+
* route: "/incidents?action=create",
|
|
110
|
+
* requiredPermissions: [permissions.incidentManage],
|
|
111
|
+
* },
|
|
112
|
+
* ],
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @example Custom entity provider:
|
|
117
|
+
* ```ts
|
|
118
|
+
* registerSearchProvider({
|
|
119
|
+
* pluginMetadata,
|
|
120
|
+
* provider: {
|
|
121
|
+
* id: "systems",
|
|
122
|
+
* name: "Systems",
|
|
123
|
+
* priority: 100,
|
|
124
|
+
* search: async (query) => {
|
|
125
|
+
* const systems = await db.select().from(schema.systems);
|
|
126
|
+
* return systems.filter(s => s.name.includes(query)).map(s => ({
|
|
127
|
+
* id: s.id,
|
|
128
|
+
* type: "entity",
|
|
129
|
+
* title: s.name,
|
|
130
|
+
* category: "Systems",
|
|
131
|
+
* route: `/catalog/systems/${s.id}`,
|
|
132
|
+
* }));
|
|
133
|
+
* },
|
|
134
|
+
* },
|
|
135
|
+
* });
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function registerSearchProvider(
|
|
139
|
+
options: RegisterSearchProviderOptions
|
|
140
|
+
): void {
|
|
141
|
+
const { pluginMetadata, commands, provider } = options;
|
|
142
|
+
|
|
143
|
+
// Register commands as a search provider
|
|
144
|
+
if (commands && commands.length > 0) {
|
|
145
|
+
const commandProvider = createCommandProvider(pluginMetadata, commands);
|
|
146
|
+
searchProviders.set(commandProvider.id, commandProvider);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Register custom provider with auto-qualified permissions
|
|
150
|
+
if (provider) {
|
|
151
|
+
const qualifiedProvider = createQualifiedProvider(pluginMetadata, provider);
|
|
152
|
+
searchProviders.set(qualifiedProvider.id, qualifiedProvider);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a search provider from command definitions.
|
|
158
|
+
*/
|
|
159
|
+
function createCommandProvider(
|
|
160
|
+
pluginMetadata: PluginMetadata,
|
|
161
|
+
commands: CommandDefinition[]
|
|
162
|
+
): BackendSearchProvider {
|
|
163
|
+
// Pre-qualify all permissions and build SearchResult objects
|
|
164
|
+
const searchableCommands: SearchResult[] = commands.map((cmd) => ({
|
|
165
|
+
id: `${pluginMetadata.pluginId}.${cmd.id}`,
|
|
166
|
+
type: "command" as const,
|
|
167
|
+
title: cmd.title,
|
|
168
|
+
subtitle: cmd.subtitle,
|
|
169
|
+
iconName: cmd.iconName,
|
|
170
|
+
category: capitalize(pluginMetadata.pluginId),
|
|
171
|
+
shortcuts: cmd.shortcuts,
|
|
172
|
+
route: cmd.route,
|
|
173
|
+
requiredPermissions: cmd.requiredPermissions?.map((perm) =>
|
|
174
|
+
qualifyPermissionId(pluginMetadata, perm)
|
|
175
|
+
),
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
id: `${pluginMetadata.pluginId}.commands`,
|
|
180
|
+
name: `${capitalize(pluginMetadata.pluginId)} Commands`,
|
|
181
|
+
priority: 80, // Commands have medium-high priority
|
|
182
|
+
search: (query) => {
|
|
183
|
+
const q = query.toLowerCase();
|
|
184
|
+
|
|
185
|
+
// Return all if no query
|
|
186
|
+
if (!q) return searchableCommands;
|
|
187
|
+
|
|
188
|
+
// Filter by title, subtitle, or category
|
|
189
|
+
return searchableCommands.filter(
|
|
190
|
+
(cmd) =>
|
|
191
|
+
cmd.title.toLowerCase().includes(q) ||
|
|
192
|
+
cmd.subtitle?.toLowerCase().includes(q) ||
|
|
193
|
+
cmd.category.toLowerCase().includes(q)
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Wrap a custom provider to auto-qualify permissions in results.
|
|
201
|
+
*/
|
|
202
|
+
function createQualifiedProvider(
|
|
203
|
+
pluginMetadata: PluginMetadata,
|
|
204
|
+
provider: BackendSearchProvider
|
|
205
|
+
): BackendSearchProvider {
|
|
206
|
+
return {
|
|
207
|
+
...provider,
|
|
208
|
+
id: `${pluginMetadata.pluginId}.${provider.id}`,
|
|
209
|
+
search: async (query, context) => {
|
|
210
|
+
const results = await provider.search(query, context);
|
|
211
|
+
|
|
212
|
+
// Auto-qualify permission IDs in results
|
|
213
|
+
return results.map((result) => ({
|
|
214
|
+
...result,
|
|
215
|
+
id: result.id.includes(".")
|
|
216
|
+
? result.id
|
|
217
|
+
: `${pluginMetadata.pluginId}.${result.id}`,
|
|
218
|
+
requiredPermissions: result.requiredPermissions?.map((permId) =>
|
|
219
|
+
// If already qualified (contains the plugin ID prefix), leave it
|
|
220
|
+
permId.startsWith(`${pluginMetadata.pluginId}.`)
|
|
221
|
+
? permId
|
|
222
|
+
: `${pluginMetadata.pluginId}.${permId}`
|
|
223
|
+
),
|
|
224
|
+
}));
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Capitalize first letter of a string.
|
|
231
|
+
*/
|
|
232
|
+
function capitalize(str: string): string {
|
|
233
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Unregister a search provider.
|
|
238
|
+
*/
|
|
239
|
+
export function unregisterSearchProvider(providerId: string): void {
|
|
240
|
+
searchProviders.delete(providerId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Unregister all search providers for a given plugin ID.
|
|
245
|
+
* Called automatically when a plugin is deregistered.
|
|
246
|
+
* @returns The number of providers removed
|
|
247
|
+
*/
|
|
248
|
+
export function unregisterProvidersByPluginId(pluginId: string): number {
|
|
249
|
+
let removed = 0;
|
|
250
|
+
for (const [id] of searchProviders) {
|
|
251
|
+
if (id.startsWith(`${pluginId}.`)) {
|
|
252
|
+
searchProviders.delete(id);
|
|
253
|
+
removed++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return removed;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get all registered search providers, sorted by priority (descending).
|
|
261
|
+
* @internal Used by the command router
|
|
262
|
+
*/
|
|
263
|
+
export function getSearchProviders(): BackendSearchProvider[] {
|
|
264
|
+
return [...searchProviders.values()].toSorted(
|
|
265
|
+
(a, b) => (b.priority ?? 0) - (a.priority ?? 0)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clear all registrations. Useful for testing.
|
|
271
|
+
*/
|
|
272
|
+
export function clearRegistry(): void {
|
|
273
|
+
searchProviders.clear();
|
|
274
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { implement } from "@orpc/server";
|
|
2
|
+
import {
|
|
3
|
+
autoAuthMiddleware,
|
|
4
|
+
type RpcContext,
|
|
5
|
+
} from "@checkstack/backend-api";
|
|
6
|
+
import {
|
|
7
|
+
commandContract,
|
|
8
|
+
filterByPermissions,
|
|
9
|
+
type SearchResult,
|
|
10
|
+
} from "@checkstack/command-common";
|
|
11
|
+
import { getSearchProviders } from "./registry";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates the command router using contract-based implementation.
|
|
15
|
+
*
|
|
16
|
+
* Auth and permissions are automatically enforced via autoAuthMiddleware
|
|
17
|
+
* based on the contract's meta.userType and meta.permissions.
|
|
18
|
+
*/
|
|
19
|
+
const os = implement(commandContract)
|
|
20
|
+
.$context<RpcContext>()
|
|
21
|
+
.use(autoAuthMiddleware);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract permissions from the context user.
|
|
25
|
+
* Only RealUser and ApplicationUser have permissions; ServiceUser doesn't.
|
|
26
|
+
*/
|
|
27
|
+
function getUserPermissions(context: RpcContext): string[] {
|
|
28
|
+
const user = context.user;
|
|
29
|
+
if (!user) return [];
|
|
30
|
+
if (user.type === "user" || user.type === "application") {
|
|
31
|
+
return user.permissions ?? [];
|
|
32
|
+
}
|
|
33
|
+
// ServiceUser has no permissions array - treated as having all permissions
|
|
34
|
+
// but for search filtering, return empty (no filtering applied)
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const createCommandRouter = () => {
|
|
39
|
+
/**
|
|
40
|
+
* Search across all registered search providers.
|
|
41
|
+
* Results are aggregated from all providers, filtered by permissions,
|
|
42
|
+
* and returned in priority order.
|
|
43
|
+
*/
|
|
44
|
+
const search = os.search.handler(async ({ input, context }) => {
|
|
45
|
+
const providers = getSearchProviders();
|
|
46
|
+
const query = input.query.toLowerCase().trim();
|
|
47
|
+
|
|
48
|
+
// Get user permissions for filtering
|
|
49
|
+
const userPermissions = getUserPermissions(context);
|
|
50
|
+
|
|
51
|
+
// Execute all provider searches in parallel
|
|
52
|
+
const providerResults = await Promise.all(
|
|
53
|
+
providers.map(async (provider) => {
|
|
54
|
+
try {
|
|
55
|
+
const results = await provider.search(query, { userPermissions });
|
|
56
|
+
return results;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Log but don't fail - one failing provider shouldn't break search
|
|
59
|
+
console.error(`Search provider ${provider.id} failed:`, error);
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Flatten and filter by permissions
|
|
66
|
+
const allResults = providerResults.flat();
|
|
67
|
+
return filterByPermissions(allResults, userPermissions);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all registered commands for browsing.
|
|
72
|
+
* Returns commands filtered by user permissions.
|
|
73
|
+
*/
|
|
74
|
+
const getCommands = os.getCommands.handler(async ({ context }) => {
|
|
75
|
+
const providers = getSearchProviders();
|
|
76
|
+
const userPermissions = getUserPermissions(context);
|
|
77
|
+
|
|
78
|
+
// Get all results with empty query (commands return all when query is empty)
|
|
79
|
+
const providerResults = await Promise.all(
|
|
80
|
+
providers.map(async (provider) => {
|
|
81
|
+
try {
|
|
82
|
+
// Empty query = return all items
|
|
83
|
+
const results = await provider.search("", { userPermissions });
|
|
84
|
+
// Filter to only commands for this endpoint
|
|
85
|
+
return results.filter(
|
|
86
|
+
(r): r is SearchResult & { type: "command" } => r.type === "command"
|
|
87
|
+
);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`Search provider ${provider.id} failed:`, error);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const allCommands = providerResults.flat();
|
|
96
|
+
return filterByPermissions(allCommands, userPermissions);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return os.router({
|
|
100
|
+
search,
|
|
101
|
+
getCommands,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type CommandRouter = ReturnType<typeof createCommandRouter>;
|