@omercnet/paseo-omp 0.2.1-next.72.1

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/SUPPORT.md +42 -0
  5. package/TESTING.md +150 -0
  6. package/client/composer-pill-settings.tsx +157 -0
  7. package/client/hub-icon.tsx +12 -0
  8. package/client/hub-popover.tsx +132 -0
  9. package/client/hub-status.ts +29 -0
  10. package/client/mcp-authorization.tsx +168 -0
  11. package/client/mcp-popover.tsx +155 -0
  12. package/client/memory-panel.tsx +76 -0
  13. package/client/memory-popover.tsx +74 -0
  14. package/client/omp-config-surface.tsx +1433 -0
  15. package/client/omp-doc-links.ts +117 -0
  16. package/client/omp-plugin-manager.tsx +1004 -0
  17. package/client/omp-store-picker.tsx +89 -0
  18. package/client/omp-store-state.ts +45 -0
  19. package/client/provider-diagnostics-state.ts +262 -0
  20. package/client/provider-icon.tsx +27 -0
  21. package/client/provider-image.tsx +66 -0
  22. package/client/quota-popover.tsx +155 -0
  23. package/client/quota-state.ts +140 -0
  24. package/client/sessions-popover.tsx +78 -0
  25. package/docs/alpha-release-checklist.md +68 -0
  26. package/docs/configuration.md +126 -0
  27. package/docs/core-provider-issue-audit.md +108 -0
  28. package/docs/images/mcp-authorization-compact.png +0 -0
  29. package/docs/images/mcp-controls-wide.png +0 -0
  30. package/docs/images/plugin-manager.png +0 -0
  31. package/docs/images/workspace-settings.png +0 -0
  32. package/docs/installation.md +67 -0
  33. package/index.client.tsx +488 -0
  34. package/index.server.ts +81 -0
  35. package/package.json +84 -0
  36. package/paseo-plugin.json +5 -0
  37. package/scripts/prepare-dependencies.mjs +20 -0
  38. package/server/hub.ts +145 -0
  39. package/server/mcp-browser.ts +95 -0
  40. package/server/memory.ts +86 -0
  41. package/server/mutation-queue.ts +12 -0
  42. package/server/omp-config.ts +135 -0
  43. package/server/omp-plugins.ts +676 -0
  44. package/server/omp-settings.ts +499 -0
  45. package/server/paths.ts +181 -0
  46. package/server/provider/catalog.ts +172 -0
  47. package/server/provider/config-normalization.ts +148 -0
  48. package/server/provider/connection.ts +1196 -0
  49. package/server/provider/host-tools.ts +777 -0
  50. package/server/provider/image.ts +143 -0
  51. package/server/provider/mcp-transport.ts +394 -0
  52. package/server/provider/omp-rpc.ts +2806 -0
  53. package/server/provider/omp.svg +5 -0
  54. package/server/provider/profile-providers.ts +249 -0
  55. package/server/provider/provider-options.ts +27 -0
  56. package/server/provider/registration.ts +162 -0
  57. package/server/provider/security.ts +317 -0
  58. package/server/provider/session-descriptors.ts +736 -0
  59. package/server/provider/session.ts +4796 -0
  60. package/server/provider/settings.ts +78 -0
  61. package/server/provider/subsessions.ts +850 -0
  62. package/server/provider/timeline-projector.ts +1801 -0
  63. package/server/provider-diagnostics.ts +1143 -0
  64. package/server/quota.ts +55 -0
  65. package/server/sessions.ts +58 -0
  66. package/shared/composer-pill-settings.ts +28 -0
  67. package/shared/hub.ts +43 -0
  68. package/shared/mcp.ts +47 -0
  69. package/shared/memory.ts +24 -0
  70. package/shared/omp-config.ts +85 -0
  71. package/shared/omp-plugins.ts +264 -0
  72. package/shared/omp-settings.ts +214 -0
  73. package/shared/omp-store.ts +58 -0
  74. package/shared/provider-diagnostics.ts +126 -0
  75. package/shared/provider-image.ts +160 -0
  76. package/shared/quota.ts +23 -0
  77. package/shared/sessions.ts +24 -0
  78. package/tsconfig.json +16 -0
@@ -0,0 +1,676 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { delimiter, isAbsolute, join } from "node:path";
3
+ import type { RpcInput } from "@getpaseo/plugin";
4
+ import { z } from "zod";
5
+ import {
6
+ type inspectOmpPluginConfig,
7
+ type listOmpPlugins,
8
+ type mutateOmpPlugin,
9
+ type mutateOmpPluginConfig,
10
+ OMP_PLUGIN_LIMIT,
11
+ type OmpInstalledPlugin,
12
+ OmpInstalledPluginSchema,
13
+ OmpMarketplacePluginIdSchema,
14
+ type OmpPluginConfigMutation,
15
+ type OmpPluginConfigSetting,
16
+ type OmpPluginConfigState,
17
+ type OmpPluginMutation,
18
+ OmpPluginNameSchema,
19
+ type OmpPluginState,
20
+ } from "../shared/omp-plugins";
21
+ import { SerialMutationQueue } from "./mutation-queue";
22
+ import { currentOmpEnvironment } from "./paths";
23
+ import {
24
+ type BoundedRun,
25
+ buildStatefulCommandEnv,
26
+ defaultSpawn,
27
+ resolveExecutablePath,
28
+ runBounded,
29
+ } from "./provider-diagnostics";
30
+
31
+ const LIST_OUTPUT_LIMIT = 512 * 1024;
32
+ const MUTATION_OUTPUT_LIMIT = 256 * 1024;
33
+ const CONFIG_OUTPUT_LIMIT = 256 * 1024;
34
+ const READ_TIMEOUT_MS = 15_000;
35
+ const MUTATION_TIMEOUT_MS = 120_000;
36
+ const KILL_GRACE_MS = 1_000;
37
+ const WINDOWS_DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
38
+ const DESCRIPTION_LIMIT = 1_024;
39
+ const CONFIG_DESCRIPTION_LIMIT = 512;
40
+ const FEATURE_LIMIT = 128;
41
+ const CREDENTIAL_KEY =
42
+ /(?:^|_)(?:API_KEY|ACCESS_KEY|ACCESS_TOKEN|AUTHORIZATION|COOKIE|CREDENTIAL|CREDENTIALS|OAUTH|PASSWORD|PRIVATE_KEY|REFRESH_TOKEN|SECRET|SESSION_TOKEN|TOKEN)(?:$|_)/u;
43
+ const PACKAGE_METADATA_LIMIT = 64 * 1024;
44
+
45
+ const RawNpmPluginSchema = z.object({
46
+ name: z.unknown(),
47
+ version: z.unknown(),
48
+ path: z.unknown(),
49
+ manifest: z.unknown().optional(),
50
+ enabledFeatures: z.unknown().optional(),
51
+ enabled: z.unknown(),
52
+ });
53
+ const RawMarketplaceEntrySchema = z.object({
54
+ installPath: z.unknown(),
55
+ version: z.unknown(),
56
+ enabled: z.unknown().optional(),
57
+ });
58
+ const RawMarketplacePluginSchema = z.object({
59
+ id: z.unknown(),
60
+ scope: z.unknown(),
61
+ entries: z.unknown(),
62
+ shadowedBy: z.unknown().optional(),
63
+ });
64
+ const RawPluginSettingSchema = z.object({
65
+ type: z.unknown(),
66
+ description: z.unknown().optional(),
67
+ secret: z.unknown().optional(),
68
+ default: z.unknown().optional(),
69
+ values: z.unknown().optional(),
70
+ min: z.unknown().optional(),
71
+ max: z.unknown().optional(),
72
+ step: z.unknown().optional(),
73
+ });
74
+
75
+ export interface OmpPluginDependencies {
76
+ resolveExecutable(): Promise<string | null>;
77
+ runPlugin(
78
+ executable: string,
79
+ args: readonly string[],
80
+ outputLimit: number,
81
+ timeoutMs: number,
82
+ cwd?: string,
83
+ ): Promise<BoundedRun>;
84
+ }
85
+
86
+ function validWorkspaceCwd(cwd: string | undefined): boolean {
87
+ return cwd === undefined || (isAbsolute(cwd) && !cwd.includes("\0"));
88
+ }
89
+
90
+ function boundedText(value: unknown, maximum: number): string | null {
91
+ if (typeof value !== "string" || value.length === 0 || value.length > maximum) return null;
92
+ if (/[\0\r\n]/u.test(value)) return null;
93
+ return value;
94
+ }
95
+
96
+ function featureNames(value: unknown): string[] {
97
+ const candidates = Array.isArray(value)
98
+ ? value
99
+ : value !== null && typeof value === "object"
100
+ ? Object.keys(value)
101
+ : [];
102
+ const result: string[] = [];
103
+ for (const candidate of candidates) {
104
+ const feature = boundedText(candidate, FEATURE_LIMIT);
105
+ if (!feature || result.includes(feature)) continue;
106
+ result.push(feature);
107
+ if (result.length === FEATURE_LIMIT) break;
108
+ }
109
+ return result;
110
+ }
111
+
112
+ function objectRecord(value: unknown): Record<string, unknown> {
113
+ return value !== null && typeof value === "object" && !Array.isArray(value)
114
+ ? (value as Record<string, unknown>)
115
+ : {};
116
+ }
117
+
118
+ function parseNpmPlugin(raw: unknown): OmpInstalledPlugin | null {
119
+ const candidate = RawNpmPluginSchema.safeParse(raw);
120
+ if (!candidate.success) return null;
121
+ const name = boundedText(candidate.data.name, 214);
122
+ const version = boundedText(candidate.data.version, 128);
123
+ const path = boundedText(candidate.data.path, 4_096);
124
+ if (!name || !version || !path || typeof candidate.data.enabled !== "boolean") return null;
125
+
126
+ const manifest = objectRecord(candidate.data.manifest);
127
+ const description = boundedText(manifest.description, DESCRIPTION_LIMIT);
128
+ const availableFeatures = featureNames(manifest.features);
129
+ const enabledFeatures = featureNames(candidate.data.enabledFeatures);
130
+ const plugin = {
131
+ id: name,
132
+ packageName: name,
133
+ version,
134
+ source: "npm" as const,
135
+ scope: null,
136
+ enabled: candidate.data.enabled,
137
+ shadowed: false,
138
+ path,
139
+ configAmbiguous: false,
140
+ description,
141
+ enabledFeatures,
142
+ availableFeatures,
143
+ configurable: Object.keys(objectRecord(manifest.settings)).length > 0,
144
+ ambiguous: false,
145
+ usesDefaultFeatures: candidate.data.enabledFeatures == null,
146
+ };
147
+ return OmpInstalledPluginSchema.safeParse(plugin).success ? plugin : null;
148
+ }
149
+
150
+ function parseMarketplacePlugin(raw: unknown): OmpInstalledPlugin | null {
151
+ const candidate = RawMarketplacePluginSchema.safeParse(raw);
152
+ if (!candidate.success || !Array.isArray(candidate.data.entries)) return null;
153
+ const id = boundedText(candidate.data.id, 128);
154
+ const scope: "user" | "project" | null =
155
+ candidate.data.scope === "user"
156
+ ? "user"
157
+ : candidate.data.scope === "project"
158
+ ? "project"
159
+ : null;
160
+ const first = RawMarketplaceEntrySchema.safeParse(candidate.data.entries[0]);
161
+ if (!id || !OmpMarketplacePluginIdSchema.safeParse(id).success || !scope || !first.success) {
162
+ return null;
163
+ }
164
+ const path = boundedText(first.data.installPath, 4_096);
165
+ const version = boundedText(first.data.version, 128);
166
+ if (!path || !version) return null;
167
+
168
+ const plugin = {
169
+ id,
170
+ version,
171
+ source: "marketplace" as const,
172
+ scope,
173
+ enabled: first.data.enabled !== false,
174
+ shadowed: candidate.data.shadowedBy === "project",
175
+ path,
176
+ description: null,
177
+ configAmbiguous: false,
178
+ enabledFeatures: [],
179
+ availableFeatures: [],
180
+ configurable: false,
181
+ ambiguous: false,
182
+ usesDefaultFeatures: true,
183
+ };
184
+ return OmpInstalledPluginSchema.safeParse(plugin).success ? plugin : null;
185
+ }
186
+
187
+ function pluginAliases(plugin: OmpInstalledPlugin): string[] {
188
+ const aliases = new Set<string>();
189
+ if (plugin.packageName) aliases.add(plugin.packageName);
190
+ aliases.add(plugin.id);
191
+ if (plugin.source === "marketplace") aliases.add(plugin.id.split("@")[0] ?? plugin.id);
192
+ return [...aliases];
193
+ }
194
+
195
+ function markAmbiguousPlugins(plugins: OmpInstalledPlugin[]): OmpInstalledPlugin[] {
196
+ const lifecycleCounts = new Map<string, number>();
197
+ const configCounts = new Map<string, number>();
198
+ const lifecycleKey = (plugin: OmpInstalledPlugin) =>
199
+ plugin.source === "marketplace"
200
+ ? `marketplace:${plugin.scope}:${plugin.id}`
201
+ : `npm:${plugin.id}`;
202
+ for (const plugin of plugins) {
203
+ const key = lifecycleKey(plugin);
204
+ lifecycleCounts.set(key, (lifecycleCounts.get(key) ?? 0) + 1);
205
+ if (plugin.packageName) {
206
+ configCounts.set(plugin.packageName, (configCounts.get(plugin.packageName) ?? 0) + 1);
207
+ }
208
+ }
209
+ return plugins.map((plugin) => ({
210
+ ...plugin,
211
+ ambiguous: (lifecycleCounts.get(lifecycleKey(plugin)) ?? 0) > 1,
212
+ configAmbiguous:
213
+ plugin.packageName !== undefined && (configCounts.get(plugin.packageName) ?? 0) > 1,
214
+ }));
215
+ }
216
+
217
+ export function parseOmpPluginList(raw: unknown): Pick<OmpPluginState, "plugins" | "droppedCount"> {
218
+ const root = objectRecord(raw);
219
+ if (!Array.isArray(root.npm) || !Array.isArray(root.marketplace)) {
220
+ throw new Error("OMP returned an invalid plugin list");
221
+ }
222
+
223
+ const plugins: OmpInstalledPlugin[] = [];
224
+ let droppedCount = 0;
225
+ const append = (plugin: OmpInstalledPlugin | null) => {
226
+ if (!plugin || plugins.length >= OMP_PLUGIN_LIMIT) {
227
+ droppedCount += 1;
228
+ return;
229
+ }
230
+ plugins.push(plugin);
231
+ };
232
+ for (const plugin of root.npm) append(parseNpmPlugin(plugin));
233
+ for (const plugin of root.marketplace) append(parseMarketplacePlugin(plugin));
234
+ return { plugins: markAmbiguousPlugins(plugins), droppedCount };
235
+ }
236
+
237
+ function runSucceeded(result: BoundedRun): boolean {
238
+ return (
239
+ result.outcome === "exited" &&
240
+ result.exitCode === 0 &&
241
+ result.signal === null &&
242
+ !result.truncated &&
243
+ !result.cleanupFailed
244
+ );
245
+ }
246
+
247
+ function unavailableState(error: string): OmpPluginState {
248
+ return { available: false, plugins: [], droppedCount: 0, error };
249
+ }
250
+
251
+ function readFailure(result: BoundedRun): string {
252
+ if (result.cleanupFailed) return "OMP plugin process cleanup could not be confirmed.";
253
+ if (result.outcome === "timeout") return "OMP plugin discovery timed out.";
254
+ return "OMP plugin discovery failed.";
255
+ }
256
+
257
+ async function resolveOmpExecutable(): Promise<string | null> {
258
+ return resolveExecutablePath(
259
+ currentOmpEnvironment().OMP_COMMAND ?? "omp",
260
+ (currentOmpEnvironment().PATH ?? "").split(delimiter),
261
+ {
262
+ cwd: process.cwd(),
263
+ platform: process.platform,
264
+ pathExt: currentOmpEnvironment().PATHEXT ?? WINDOWS_DEFAULT_PATHEXT,
265
+ },
266
+ );
267
+ }
268
+
269
+ async function runOmpPlugin(
270
+ executable: string,
271
+ args: readonly string[],
272
+ outputLimit: number,
273
+ timeoutMs: number,
274
+ cwd = process.cwd(),
275
+ ): Promise<BoundedRun> {
276
+ return runBounded(
277
+ defaultSpawn,
278
+ executable,
279
+ args,
280
+ buildStatefulCommandEnv(currentOmpEnvironment()),
281
+ timeoutMs,
282
+ KILL_GRACE_MS,
283
+ outputLimit,
284
+ cwd,
285
+ );
286
+ }
287
+
288
+ async function enrichMarketplaceConfiguration(
289
+ plugins: OmpInstalledPlugin[],
290
+ ): Promise<OmpInstalledPlugin[]> {
291
+ const enriched = await Promise.all(
292
+ plugins.map(async (plugin) => {
293
+ if (plugin.source !== "marketplace" || !plugin.path) return plugin;
294
+ try {
295
+ const metadataPath = join(plugin.path, "package.json");
296
+ const metadata = await stat(metadataPath);
297
+ if (!metadata.isFile() || metadata.size > PACKAGE_METADATA_LIMIT) return plugin;
298
+ const raw: unknown = JSON.parse(await readFile(metadataPath, "utf8"));
299
+ const metadataName = OmpPluginNameSchema.safeParse(objectRecord(raw).name);
300
+ const fallbackName = OmpPluginNameSchema.safeParse(plugin.id.split("@")[0]);
301
+ const packageName = metadataName.success
302
+ ? metadataName.data
303
+ : fallbackName.success
304
+ ? fallbackName.data
305
+ : null;
306
+ if (!packageName) return plugin;
307
+ return { ...plugin, packageName, configurable: true };
308
+ } catch {
309
+ return plugin;
310
+ }
311
+ }),
312
+ );
313
+ return markAmbiguousPlugins(enriched);
314
+ }
315
+
316
+ const DEFAULT_DEPENDENCIES: OmpPluginDependencies = {
317
+ resolveExecutable: resolveOmpExecutable,
318
+ runPlugin: runOmpPlugin,
319
+ };
320
+
321
+ async function loadPluginState(
322
+ executable: string | null | undefined,
323
+ dependencies: OmpPluginDependencies,
324
+ cwd?: string,
325
+ ): Promise<OmpPluginState> {
326
+ const resolved = executable === undefined ? await dependencies.resolveExecutable() : executable;
327
+ if (!resolved) return unavailableState("The OMP executable could not be resolved.");
328
+ const result = await dependencies.runPlugin(
329
+ resolved,
330
+ ["plugin", "list", "--json"],
331
+ LIST_OUTPUT_LIMIT,
332
+ READ_TIMEOUT_MS,
333
+ cwd,
334
+ );
335
+ if (!runSucceeded(result)) return unavailableState(readFailure(result));
336
+ try {
337
+ const parsed = parseOmpPluginList(JSON.parse(result.stdout) as unknown);
338
+ return {
339
+ available: true,
340
+ plugins: await enrichMarketplaceConfiguration(parsed.plugins),
341
+ droppedCount: parsed.droppedCount,
342
+ };
343
+ } catch {
344
+ return unavailableState("OMP returned invalid plugin metadata.");
345
+ }
346
+ }
347
+
348
+ export async function listOmpPluginsWithDependencies(
349
+ input: RpcInput<typeof listOmpPlugins>,
350
+ dependencies: OmpPluginDependencies,
351
+ ): Promise<OmpPluginState> {
352
+ if (!validWorkspaceCwd(input.cwd)) return unavailableState("The workspace path is invalid.");
353
+ return loadPluginState(undefined, dependencies, input.cwd);
354
+ }
355
+
356
+ export async function resolveListOmpPlugins(
357
+ input: RpcInput<typeof listOmpPlugins>,
358
+ ): Promise<OmpPluginState> {
359
+ return listOmpPluginsWithDependencies(input, DEFAULT_DEPENDENCIES);
360
+ }
361
+
362
+ function isCredentialSetting(key: string, secret: unknown): boolean {
363
+ if (secret === true) return true;
364
+ const normalized = key
365
+ .replace(/([a-z0-9])([A-Z])/gu, "$1_$2")
366
+ .replace(/[^A-Za-z0-9]+/gu, "_")
367
+ .toUpperCase();
368
+ return CREDENTIAL_KEY.test(normalized);
369
+ }
370
+
371
+ export function parseOmpPluginConfig(raw: unknown): {
372
+ settings: OmpPluginConfigSetting[];
373
+ droppedCount: number;
374
+ } {
375
+ const root = objectRecord(raw);
376
+ const values = objectRecord(root.settings);
377
+ const schema = objectRecord(root.schema);
378
+ const settings: OmpPluginConfigSetting[] = [];
379
+ let droppedCount = 0;
380
+ for (const [key, rawDefinition] of Object.entries(schema)) {
381
+ if (settings.length >= OMP_PLUGIN_LIMIT || !boundedText(key, 128)) {
382
+ droppedCount += 1;
383
+ continue;
384
+ }
385
+ const definition = RawPluginSettingSchema.safeParse(rawDefinition);
386
+ if (!definition.success) {
387
+ droppedCount += 1;
388
+ continue;
389
+ }
390
+ const type = definition.data.type;
391
+ if (type !== "string" && type !== "number" && type !== "boolean" && type !== "enum") {
392
+ droppedCount += 1;
393
+ continue;
394
+ }
395
+ const description =
396
+ typeof definition.data.description === "string"
397
+ ? definition.data.description.slice(0, CONFIG_DESCRIPTION_LIMIT)
398
+ : "";
399
+ const enumValues =
400
+ type === "enum" && Array.isArray(definition.data.values)
401
+ ? definition.data.values
402
+ .map((value) => boundedText(value, 256))
403
+ .filter((value): value is string => value !== null)
404
+ .slice(0, FEATURE_LIMIT)
405
+ : [];
406
+ const minimum =
407
+ typeof definition.data.min === "number" && Number.isFinite(definition.data.min)
408
+ ? definition.data.min
409
+ : undefined;
410
+ const maximum =
411
+ typeof definition.data.max === "number" && Number.isFinite(definition.data.max)
412
+ ? definition.data.max
413
+ : undefined;
414
+ const step =
415
+ typeof definition.data.step === "number" &&
416
+ Number.isFinite(definition.data.step) &&
417
+ definition.data.step > 0
418
+ ? definition.data.step
419
+ : undefined;
420
+ settings.push({
421
+ key,
422
+ type,
423
+ description,
424
+ configured: Object.hasOwn(values, key),
425
+ secret: isCredentialSetting(key, definition.data.secret),
426
+ enumValues,
427
+ ...(minimum === undefined ? {} : { minimum }),
428
+ ...(maximum === undefined ? {} : { maximum }),
429
+ ...(step === undefined ? {} : { step }),
430
+ });
431
+ }
432
+ return { settings, droppedCount };
433
+ }
434
+
435
+ function unavailableConfig(plugin: string, error: string): OmpPluginConfigState {
436
+ return { available: false, plugin, settings: [], droppedCount: 0, error };
437
+ }
438
+
439
+ async function loadPluginConfig(
440
+ executable: string,
441
+ plugin: string,
442
+ dependencies: OmpPluginDependencies,
443
+ cwd?: string,
444
+ ): Promise<OmpPluginConfigState> {
445
+ const result = await dependencies.runPlugin(
446
+ executable,
447
+ ["plugin", "config", "list", plugin, "--json"],
448
+ CONFIG_OUTPUT_LIMIT,
449
+ READ_TIMEOUT_MS,
450
+ cwd,
451
+ );
452
+ if (!runSucceeded(result)) {
453
+ return unavailableConfig(
454
+ plugin,
455
+ result.outcome === "timeout"
456
+ ? "OMP plugin configuration inspection timed out."
457
+ : "OMP plugin configuration inspection failed.",
458
+ );
459
+ }
460
+ try {
461
+ return { available: true, plugin, ...parseOmpPluginConfig(JSON.parse(result.stdout)) };
462
+ } catch {
463
+ return unavailableConfig(plugin, "OMP returned invalid plugin configuration metadata.");
464
+ }
465
+ }
466
+
467
+ export async function inspectOmpPluginConfigWithDependencies(
468
+ input: RpcInput<typeof inspectOmpPluginConfig>,
469
+ dependencies: OmpPluginDependencies,
470
+ ): Promise<OmpPluginConfigState> {
471
+ if (!validWorkspaceCwd(input.cwd)) {
472
+ return unavailableConfig(input.plugin, "The workspace path is invalid.");
473
+ }
474
+ const executable = await dependencies.resolveExecutable();
475
+ return executable
476
+ ? loadPluginConfig(executable, input.plugin, dependencies, input.cwd)
477
+ : unavailableConfig(input.plugin, "The OMP executable could not be resolved.");
478
+ }
479
+
480
+ export async function resolveInspectOmpPluginConfig(
481
+ input: RpcInput<typeof inspectOmpPluginConfig>,
482
+ ) {
483
+ return inspectOmpPluginConfigWithDependencies(input, DEFAULT_DEPENDENCIES);
484
+ }
485
+
486
+ export function buildOmpPluginConfigMutationArgs(input: OmpPluginConfigMutation): string[] {
487
+ return input.action === "set"
488
+ ? ["plugin", "config", "set", input.plugin, input.key, "--json", "--", String(input.value)]
489
+ : ["plugin", "config", "delete", input.plugin, input.key, "--json"];
490
+ }
491
+
492
+ function configValueError(
493
+ setting: OmpPluginConfigSetting,
494
+ value: string | number | boolean,
495
+ ): string | null {
496
+ if (setting.type === "string") {
497
+ return typeof value === "string" ? null : "This setting requires a string value.";
498
+ }
499
+ if (setting.type === "boolean") {
500
+ return typeof value === "boolean" ? null : "This setting requires a boolean value.";
501
+ }
502
+ if (setting.type === "enum") {
503
+ return typeof value === "string" && setting.enumValues.includes(value)
504
+ ? null
505
+ : "This setting requires one of its documented choices.";
506
+ }
507
+ if (typeof value !== "number" || !Number.isFinite(value)) {
508
+ return "This setting requires a finite number.";
509
+ }
510
+ if (setting.minimum !== undefined && value < setting.minimum) {
511
+ return "This setting is below its documented minimum.";
512
+ }
513
+ if (setting.maximum !== undefined && value > setting.maximum) {
514
+ return "This setting is above its documented maximum.";
515
+ }
516
+ return null;
517
+ }
518
+
519
+ export async function mutateOmpPluginConfigWithDependencies(
520
+ input: RpcInput<typeof mutateOmpPluginConfig>,
521
+ dependencies: OmpPluginDependencies,
522
+ ): Promise<{ ok: boolean; message: string; config: OmpPluginConfigState }> {
523
+ if (!validWorkspaceCwd(input.cwd)) {
524
+ const config = unavailableConfig(input.plugin, "The workspace path is invalid.");
525
+ return { ok: false, message: "The workspace path is invalid.", config };
526
+ }
527
+ const executable = await dependencies.resolveExecutable();
528
+ if (!executable) {
529
+ const config = unavailableConfig(input.plugin, "The OMP executable could not be resolved.");
530
+ return { ok: false, message: "The OMP executable could not be resolved.", config };
531
+ }
532
+
533
+ const current = await loadPluginConfig(executable, input.plugin, dependencies, input.cwd);
534
+ const setting = current.settings.find((candidate) => candidate.key === input.key);
535
+ if (!current.available || !setting) {
536
+ return {
537
+ ok: false,
538
+ message: current.available
539
+ ? "OMP does not advertise this plugin setting."
540
+ : "OMP plugin configuration is unavailable.",
541
+ config: current,
542
+ };
543
+ }
544
+ if (input.action === "set" && setting.secret) {
545
+ return {
546
+ ok: false,
547
+ message: "Secret plugin settings cannot be written through process arguments.",
548
+ config: current,
549
+ };
550
+ }
551
+ if (input.action === "set") {
552
+ const invalid = configValueError(setting, input.value);
553
+ if (invalid) return { ok: false, message: invalid, config: current };
554
+ }
555
+
556
+ const mutation = await dependencies.runPlugin(
557
+ executable,
558
+ buildOmpPluginConfigMutationArgs(input),
559
+ MUTATION_OUTPUT_LIMIT,
560
+ READ_TIMEOUT_MS,
561
+ input.cwd,
562
+ );
563
+ const config = await loadPluginConfig(executable, input.plugin, dependencies, input.cwd);
564
+ const mutationSucceeded = runSucceeded(mutation);
565
+ const ok = mutationSucceeded && config.available;
566
+ return {
567
+ ok,
568
+ message: !mutationSucceeded
569
+ ? mutationFailure(mutation)
570
+ : !config.available
571
+ ? "Plugin setting changed, but refreshed metadata is unavailable."
572
+ : input.action === "set"
573
+ ? "Plugin setting saved. New OMP sessions use the updated value."
574
+ : "Plugin setting deleted. New OMP sessions use its default or environment fallback.",
575
+ config,
576
+ };
577
+ }
578
+
579
+ export function resolveMutateOmpPluginConfig(
580
+ input: RpcInput<typeof mutateOmpPluginConfig>,
581
+ ): Promise<{ ok: boolean; message: string; config: OmpPluginConfigState }> {
582
+ return pluginMutationQueue.run(() =>
583
+ mutateOmpPluginConfigWithDependencies(input, DEFAULT_DEPENDENCIES),
584
+ );
585
+ }
586
+
587
+ export function buildOmpPluginMutationArgs(input: OmpPluginMutation): string[] {
588
+ const target = input.action === "install" ? input.source : input.plugin;
589
+ return ["plugin", input.action, target, "--scope", input.scope ?? "user", "--json"];
590
+ }
591
+
592
+ function mutationFailure(result: BoundedRun): string {
593
+ if (result.cleanupFailed) return "OMP plugin process cleanup could not be confirmed.";
594
+ if (result.outcome === "timeout") return "OMP plugin operation timed out.";
595
+ return "OMP rejected the plugin operation.";
596
+ }
597
+
598
+ function mutationSuccess(action: OmpPluginMutation["action"]): string {
599
+ switch (action) {
600
+ case "install":
601
+ return "Plugin installed. New runtime extensions load in the next OMP session.";
602
+ case "enable":
603
+ return "Plugin enabled. New runtime extensions load in the next OMP session.";
604
+ case "disable":
605
+ return "Plugin disabled. Active OMP sessions are unchanged.";
606
+ case "uninstall":
607
+ return "Plugin uninstalled. Active OMP sessions are unchanged.";
608
+ case "upgrade":
609
+ return "Plugin upgraded. New runtime extensions load in the next OMP session.";
610
+ }
611
+ }
612
+
613
+ const pluginMutationQueue = new SerialMutationQueue();
614
+
615
+ export async function mutateOmpPluginWithDependencies(
616
+ input: RpcInput<typeof mutateOmpPlugin>,
617
+ dependencies: OmpPluginDependencies,
618
+ ): Promise<{ ok: boolean; message: string; state: OmpPluginState }> {
619
+ if (!validWorkspaceCwd(input.cwd)) {
620
+ const state = unavailableState("The workspace path is invalid.");
621
+ return { ok: false, message: "The workspace path is invalid.", state };
622
+ }
623
+ const executable = await dependencies.resolveExecutable();
624
+ if (!executable) {
625
+ const state = unavailableState("The OMP executable could not be resolved.");
626
+ return { ok: false, message: "The OMP executable could not be resolved.", state };
627
+ }
628
+
629
+ if (input.action !== "install") {
630
+ const before = await loadPluginState(executable, dependencies, input.cwd);
631
+ if (!before.available) {
632
+ return { ok: false, message: "OMP plugin state is unavailable.", state: before };
633
+ }
634
+ const matches = before.plugins.filter(
635
+ (plugin) =>
636
+ pluginAliases(plugin).includes(input.plugin) &&
637
+ (input.scope === undefined || plugin.scope === input.scope),
638
+ );
639
+ if (matches.length !== 1 || matches[0]?.ambiguous) {
640
+ return {
641
+ ok: false,
642
+ message: "The plugin target is ambiguous in this scope.",
643
+ state: before,
644
+ };
645
+ }
646
+ if (matches[0]?.scope === "project" && !input.cwd) {
647
+ return {
648
+ ok: false,
649
+ message: "Project-scoped plugin actions require a workspace.",
650
+ state: before,
651
+ };
652
+ }
653
+ }
654
+
655
+ const mutation = await dependencies.runPlugin(
656
+ executable,
657
+ buildOmpPluginMutationArgs(input),
658
+ MUTATION_OUTPUT_LIMIT,
659
+ MUTATION_TIMEOUT_MS,
660
+ input.cwd,
661
+ );
662
+ const state = await loadPluginState(executable, dependencies, input.cwd);
663
+ return {
664
+ ok: runSucceeded(mutation),
665
+ message: runSucceeded(mutation) ? mutationSuccess(input.action) : mutationFailure(mutation),
666
+ state,
667
+ };
668
+ }
669
+
670
+ export function resolveMutateOmpPlugin(
671
+ input: RpcInput<typeof mutateOmpPlugin>,
672
+ ): Promise<{ ok: boolean; message: string; state: OmpPluginState }> {
673
+ return pluginMutationQueue.run(() =>
674
+ mutateOmpPluginWithDependencies(input, DEFAULT_DEPENDENCIES),
675
+ );
676
+ }