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