Files
NetMesh/electron/plugins/contributionService.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

616 lines
27 KiB
JavaScript

"use strict";
const { createHash } = require("node:crypto");
const { assertPluginContextKey, evaluateContextKeyExpression } = require("./contextKeys.cjs");
const { assertPluginJsonValue } = require("./jsonBoundary.cjs");
const { compileRestrictedJsonSchema } = require("./restrictedJsonSchema.cjs");
const { PluginRpcError, RPC_ERRORS } = require("./rpcRouter.cjs");
const MAX_SETTING_BYTES = 128 * 1024;
const MAX_PATTERN_LENGTH = 512;
const MAX_PATTERN_VALUE_LENGTH = 4_096;
const MAX_SCOPE_ID_LENGTH = 256;
const SETTINGS_SCOPES = new Set(["application", "workspace", "host", "session", "device"]);
function invalidArgument(message) {
return new PluginRpcError(RPC_ERRORS.invalidArgument, message);
}
function notFound(message) {
return new PluginRpcError(RPC_ERRORS.notFound, message);
}
function freezeJson(value) {
const clone = structuredClone(value);
const freeze = (item) => {
if (!item || typeof item !== "object" || Object.isFrozen(item)) return item;
for (const child of Array.isArray(item) ? item : Object.values(item)) freeze(child);
return Object.freeze(item);
};
return freeze(clone);
}
function assertJsonValue(value, label = "value") {
try { assertPluginJsonValue(value, { maxBytes: MAX_SETTING_BYTES }); }
catch (error) {
throw invalidArgument(`Plugin ${label} must be a bounded JSON value: ${error?.message ?? error}`);
}
return value;
}
function resolveLocalizedText(value, locale = "en") {
if (typeof value === "string") return value;
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
const base = locale.split("-")[0];
return value[locale] ?? value[base] ?? value.en ?? value.default ?? Object.values(value)[0] ?? "";
}
function normalizeScopeId(scope, scopeId) {
if (!SETTINGS_SCOPES.has(scope)) throw invalidArgument("Plugin setting scope is invalid");
if (scope === "application") return "application";
const value = scope === "device" && scopeId == null ? "device" : scopeId;
if (typeof value !== "string" || value.length < 1 || value.length > MAX_SCOPE_ID_LENGTH || value.includes("\0")) {
throw invalidArgument(`Plugin ${scope} setting requires a valid scope ID`);
}
return value;
}
function getContribution(manifest, kind, id) {
return manifest?.contributes?.[kind]?.find((item) => item.id === id || item.command === id) ?? null;
}
function compileSettingPattern(source, settingId = "setting") {
if (typeof source !== "string" || source.length < 1 || source.length > MAX_PATTERN_LENGTH
|| /\(\?/u.test(source) || /\\(?:[1-9]|k<)/u.test(source)
|| /\)(?:[*+?]|\{\d+(?:,\d*)?\})/u.test(source)) {
throw invalidArgument(`${settingId} has an unsafe pattern`);
}
try { return new RegExp(source, "u"); }
catch { throw invalidArgument(`${settingId} has an invalid pattern`); }
}
function assertSettingValue(setting, value) {
assertJsonValue(value, "setting value");
const options = new Set((setting.options ?? []).map((option) => option.value));
switch (setting.control) {
case "switch":
if (typeof value !== "boolean") throw invalidArgument(`${setting.id} must be a boolean`);
break;
case "radio": case "select":
if (typeof value !== "string" || (options.size && !options.has(value))) {
throw invalidArgument(`${setting.id} must use a declared option`);
}
break;
case "multiselect":
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || (options.size && !options.has(item)))
|| new Set(value).size !== value.length) {
throw invalidArgument(`${setting.id} must be a unique list of declared options`);
}
break;
case "number": case "slider":
if (typeof value !== "number" || !Number.isFinite(value)) throw invalidArgument(`${setting.id} must be finite`);
if (setting.minimum != null && value < setting.minimum) throw invalidArgument(`${setting.id} is below its minimum`);
if (setting.maximum != null && value > setting.maximum) throw invalidArgument(`${setting.id} is above its maximum`);
if (setting.step != null) {
const steps = (value - (setting.minimum ?? 0)) / setting.step;
const tolerance = Number.EPSILON * Math.max(1, Math.abs(steps)) * 16;
if (Math.abs(steps - Math.round(steps)) > tolerance) {
throw invalidArgument(`${setting.id} does not align to its step`);
}
}
break;
case "list": case "table":
if (!Array.isArray(value)) throw invalidArgument(`${setting.id} must be an array`);
try { compileRestrictedJsonSchema(setting.valueSchema, { rootType: "array" })(value); }
catch (error) { throw invalidArgument(`${setting.id} ${error?.message ?? error}`); }
break;
default:
if (typeof value !== "string") throw invalidArgument(`${setting.id} must be a string`);
if (setting.pattern) {
if (value.length > MAX_PATTERN_VALUE_LENGTH) {
throw invalidArgument(`${setting.id} exceeds the patterned text limit`);
}
const pattern = compileSettingPattern(setting.pattern, setting.id);
if (!pattern.test(value)) throw invalidArgument(`${setting.id} does not match its pattern`);
}
}
return value;
}
function secretSettingKey(settingId, scope, scopeId) {
const digest = createHash("sha256").update(`${scope}\0${scopeId}`).digest("hex").slice(0, 32);
return `setting:${settingId}:${digest}`;
}
function activationEvents(manifest) {
return new Set(manifest?.activationEvents ?? []);
}
function shouldActivateOnStartup(manifest) {
return activationEvents(manifest).has("onStartupFinished");
}
function resolvePlatformKeybinding(keybinding, platform = process.platform) {
if (!keybinding) return undefined;
if (platform === "darwin") return keybinding.mac ?? keybinding.key;
if (platform === "win32") return keybinding.windows ?? keybinding.key;
if (platform === "linux") return keybinding.linux ?? keybinding.key;
return keybinding.key;
}
function isContributionAvailable(plugin) {
return plugin?.enabled === true
&& plugin.manifest != null
&& plugin.runtime?.quarantinedAt == null;
}
class PluginContributionService {
constructor(options) {
this.database = options.database;
this.runtimeSupervisor = options.runtimeSupervisor;
this.secretStore = options.secretStore;
this.getLocale = options.getLocale ?? (() => "en");
this.contextKeys = new Map();
this.contextKeyOwners = new Map();
this.listeners = new Set();
this.viewMessageListeners = new Set();
this.environment = null;
this.environmentRevision = 0;
this.runtimeEnvironmentState = new Map();
this.initialized = false;
}
registerRpcCapabilities(registry) {
registry.registerRequest("settings.get", (params, context) => this.#runtimeGetSetting(params, context), {
metadata: { capability: "settings", mutating: false, permission: "settings.read" },
authorization: { permission: "settings.read", resources: ["*"], reason: "Read plugin settings" },
});
registry.registerRequest("settings.update", (params, context) => this.#runtimeUpdateSetting(params, context), {
metadata: { capability: "settings", mutating: true, permission: "settings.write" },
authorization: { permission: "settings.write", resources: ["*"], reason: "Update plugin settings" },
});
registry.registerRequest("commands.execute", (params, context) => this.executeCommand(params?.command, params?.args, {
source: "plugin",
callerPluginId: context.pluginId,
}), {
metadata: { capability: "commands", mutating: true, permission: "commands" },
authorization: { permission: "commands", resources: ["*"], reason: "Execute a plugin command" },
});
registry.registerRequest("contextKeys.set", (params, context) => {
const key = assertPluginContextKey(context.pluginId, params?.key);
assertJsonValue(params?.value, "Context Key value");
const owner = this.contextKeyOwners.get(key);
if (owner != null && owner !== context.pluginId) {
throw invalidArgument("Plugin Context Key is owned by another plugin");
}
this.contextKeys.set(key, freezeJson(params.value));
this.contextKeyOwners.set(key, context.pluginId);
this.#emitChange("context-key", context.pluginId);
return null;
}, {
metadata: { capability: "contextKeys", mutating: true, public: true },
});
registry.registerRequest("views.getState", (params, context) => {
const view = this.#assertOwnedContribution(context.manifest, "views", params?.viewId, context.pluginId);
const scopeId = normalizeScopeId("session", params?.scopeId);
return { state: this.database.getViewState(context.pluginId, view.id, scopeId) };
}, {
metadata: { capability: "views", mutating: false, permission: "views" },
authorization: { permission: "views", resources: ["*"], reason: "Restore plugin view state" },
});
registry.registerRequest("views.setState", (params, context) => {
const view = this.#assertOwnedContribution(context.manifest, "views", params?.viewId, context.pluginId);
const scopeId = normalizeScopeId("session", params?.scopeId);
assertJsonValue(params?.state, "view state");
this.database.setViewState(context.pluginId, view.id, scopeId, params.state);
return null;
}, {
metadata: { capability: "views", mutating: true, permission: "views" },
authorization: { permission: "views", resources: ["*"], reason: "Persist plugin view state" },
});
registry.registerNotification("views.postMessage", (params, context) => {
const view = this.#assertOwnedContribution(context.manifest, "views", params?.viewId, context.pluginId);
assertJsonValue(params?.message, "view message");
const event = freezeJson({ pluginId: context.pluginId, viewId: view.id, message: params.message });
for (const listener of [...this.viewMessageListeners]) {
try { listener(event); } catch {}
}
}, {
metadata: { capability: "views", mutating: true, permission: "views" },
authorization: { permission: "views", resources: ["*"], reason: "Message a plugin view" },
});
}
async initialize() {
if (this.initialized) return;
this.initialized = true;
for (const plugin of this.database.listPlugins()) {
if (!plugin.enabled || plugin.runtime.quarantinedAt != null || !shouldActivateOnStartup(plugin.manifest)) continue;
try { await this.#startPlugin(plugin.id); } catch {}
}
this.#emitChange("initialized");
}
onDidChange(listener) {
if (typeof listener !== "function") throw new TypeError("Plugin contribution listener must be a function");
this.listeners.add(listener);
return Object.freeze({ dispose: () => this.listeners.delete(listener) });
}
onDidPostViewMessage(listener) {
if (typeof listener !== "function") throw new TypeError("Plugin view listener must be a function");
this.viewMessageListeners.add(listener);
return Object.freeze({ dispose: () => this.viewMessageListeners.delete(listener) });
}
#emitChange(reason, pluginId = null) {
const event = Object.freeze({ reason, pluginId, revision: Date.now() });
for (const listener of [...this.listeners]) {
try { listener(event); } catch {}
}
}
async onPluginEnabled(pluginId) {
const plugin = this.database.getActivePlugin(pluginId);
if (!plugin?.enabled) throw notFound(`Plugin is not enabled: ${pluginId}`);
if (shouldActivateOnStartup(plugin.manifest)) await this.#startPlugin(pluginId);
this.#emitChange("plugin-enabled", pluginId);
return plugin;
}
onPluginDisabled(pluginId) {
this.#clearRuntimeOwnedState(pluginId);
this.#emitChange("plugin-disabled", pluginId);
}
onRuntimeStateChanged(event) {
if (!["stopped", "error", "quarantined"].includes(event?.status) || typeof event.pluginId !== "string") return;
this.#clearRuntimeOwnedState(event.pluginId);
this.#emitChange(`runtime-${event.status}`, event.pluginId);
}
getEnvironment() {
return this.environment;
}
#clearRuntimeOwnedState(pluginId) {
this.runtimeEnvironmentState.delete(pluginId);
for (const [key, owner] of [...this.contextKeyOwners.entries()]) {
if (owner !== pluginId) continue;
this.contextKeyOwners.delete(key);
this.contextKeys.delete(key);
}
}
#findContribution(kind, id) {
if (typeof id !== "string") throw invalidArgument(`Plugin ${kind} ID is required`);
for (const plugin of this.database.listPlugins()) {
if (!isContributionAvailable(plugin)) continue;
const contribution = getContribution(plugin.manifest, kind, id);
if (contribution) return { plugin, contribution };
}
throw notFound(`Plugin ${kind} contribution was not found: ${id}`);
}
#assertOwnedContribution(manifest, kind, id, pluginId) {
if (typeof id !== "string" || !id.startsWith(`${pluginId}.`)) throw invalidArgument(`Plugin ${kind} ID is invalid`);
const contribution = getContribution(manifest, kind, id);
if (!contribution) throw notFound(`Plugin ${kind} contribution was not found: ${id}`);
return contribution;
}
async #ensureActivated(plugin, event) {
const events = activationEvents(plugin.manifest);
const isContributionEvent = /^(?:onCommand|onView|onProvider):/u.test(event);
if (!events.has(event) && !events.has("onStartupFinished") && !isContributionEvent) {
throw new PluginRpcError(RPC_ERRORS.failedPrecondition, `Plugin activation event is not declared: ${event}`);
}
// Declared UI contributions are implicit lazy activation points. This keeps
// manifest activationEvents backward compatible while avoiding eager start.
return this.#startPlugin(plugin.id);
}
async #startPlugin(pluginId) {
const identity = await this.runtimeSupervisor.start(pluginId);
if (this.environment == null) return identity;
const runtimeId = identity?.runtimeId ?? null;
const seeded = this.runtimeEnvironmentState.get(pluginId);
if (runtimeId != null && seeded?.runtimeId === runtimeId && seeded.revision === this.environmentRevision) {
return identity;
}
await this.runtimeSupervisor.notify(pluginId, "plugin.environment.changed", this.environment);
if (runtimeId != null) {
this.runtimeEnvironmentState.set(pluginId, { runtimeId, revision: this.environmentRevision });
}
return identity;
}
async executeCommand(commandId, args, invocation = {}) {
const { plugin, contribution } = this.#findContribution("commands", commandId);
if (invocation.callerPluginId && invocation.callerPluginId !== plugin.id) {
throw new PluginRpcError(RPC_ERRORS.permissionDenied, "Plugins may execute only their own declared commands");
}
const keys = this.#context(invocation.context);
if (!evaluateContextKeyExpression(contribution.enablement, keys)) {
throw new PluginRpcError(RPC_ERRORS.failedPrecondition, `Plugin command is disabled: ${commandId}`);
}
assertJsonValue(args ?? null, "command arguments");
await this.#ensureActivated(plugin, `onCommand:${commandId}`);
return this.runtimeSupervisor.request(plugin.id, "plugin.command.execute", {
command: commandId,
...(args === undefined ? {} : { args }),
invocation: freezeJson({ source: invocation.source ?? "host", context: invocation.context ?? {} }),
});
}
async activateView(viewId, params = {}) {
const { plugin, contribution } = this.#findContribution("views", viewId);
const keys = this.#context(params.context);
if (!evaluateContextKeyExpression(contribution.when, keys)) {
throw new PluginRpcError(RPC_ERRORS.failedPrecondition, `Plugin view is unavailable: ${viewId}`);
}
const identity = await this.#ensureActivated(plugin, `onView:${viewId}`);
return { plugin, view: contribution, identity: identity ?? null };
}
assertViewActivationCurrent(expected, params = {}) {
if (!expected || typeof expected !== "object") throw invalidArgument("Plugin view activation identity is invalid");
const { plugin, contribution } = this.#findContribution("views", expected.viewId);
if (plugin.id !== expected.pluginId || plugin.activeVersion !== expected.pluginVersion) {
throw new PluginRpcError(RPC_ERRORS.failedPrecondition, "Plugin view ownership changed while opening");
}
const keys = this.#context(params.context);
if (!evaluateContextKeyExpression(contribution.when, keys)) {
throw new PluginRpcError(RPC_ERRORS.failedPrecondition, `Plugin view is unavailable: ${expected.viewId}`);
}
const identity = this.runtimeSupervisor.getRuntimeIdentity?.(plugin.id) ?? null;
if (!identity?.runtimeId || identity.runtimeId !== expected.runtimeId
|| identity.pluginVersion !== expected.pluginVersion) {
throw new PluginRpcError(RPC_ERRORS.failedPrecondition, "Plugin view runtime ownership changed while opening");
}
return { plugin, view: contribution, identity };
}
async activateProvider(providerId) {
const { plugin, contribution } = this.#findContribution("providers", providerId);
const identity = await this.#ensureActivated(plugin, `onProvider:${providerId}`);
return Object.freeze({ plugin, provider: contribution, identity: identity ?? null });
}
listProviders(options = {}) {
if (options.kind != null && (typeof options.kind !== "string" || options.kind.length < 1)) {
throw invalidArgument("Plugin Provider kind is invalid");
}
const locale = options.locale ?? this.getLocale();
const providers = [];
for (const plugin of this.database.listPlugins()) {
if (!isContributionAvailable(plugin)) continue;
for (const provider of plugin.manifest.contributes?.providers ?? []) {
if (options.kind != null && provider.kind !== options.kind) continue;
providers.push({
pluginId: plugin.id,
pluginVersion: plugin.activeVersion,
pluginDisplayName: resolveLocalizedText(
plugin.manifest.displayName ?? plugin.manifest.name,
locale,
),
provider: {
...provider,
label: resolveLocalizedText(provider.label, locale),
description: provider.description == null
? undefined
: resolveLocalizedText(provider.description, locale),
},
});
}
}
return freezeJson(providers);
}
#settingRecord(pluginId, settingId) {
const plugin = this.database.getActivePlugin(pluginId);
if (!isContributionAvailable(plugin)) throw notFound(`Plugin is not enabled: ${pluginId}`);
const setting = this.#assertOwnedContribution(plugin.manifest, "settings", settingId, pluginId);
return { plugin, setting };
}
async getSetting(pluginId, settingId, scopeId) {
const { setting } = this.#settingRecord(pluginId, settingId);
const normalizedScopeId = normalizeScopeId(setting.scope, scopeId);
if (setting.secret) {
return this.secretStore.getReference(pluginId, secretSettingKey(setting.id, setting.scope, normalizedScopeId)) ?? undefined;
}
const stored = this.database.getSetting(pluginId, setting.id, setting.scope, normalizedScopeId);
return stored === undefined ? freezeJson(setting.default) : stored;
}
async updateSetting(pluginId, settingId, value, scopeId, options = {}) {
const { setting } = this.#settingRecord(pluginId, settingId);
const normalizedScopeId = normalizeScopeId(setting.scope, scopeId);
assertSettingValue(setting, value);
if (setting.secret) {
this.secretStore.set(pluginId, secretSettingKey(setting.id, setting.scope, normalizedScopeId), value);
} else {
this.database.setSetting(pluginId, setting.id, setting.scope, normalizedScopeId, value);
}
this.#emitChange("setting-updated", pluginId);
try {
await this.runtimeSupervisor.notify(pluginId, "plugin.settings.changed", {
settingId: setting.id,
scope: setting.scope,
scopeId: normalizedScopeId,
source: options.source ?? "host",
});
} catch {}
return { restartRequired: setting.restartRequired === true };
}
async resetSetting(pluginId, settingId, scopeId) {
const { setting } = this.#settingRecord(pluginId, settingId);
if (setting.required && setting.default === undefined) throw invalidArgument(`${setting.id} is required`);
const normalizedScopeId = normalizeScopeId(setting.scope, scopeId);
if (setting.secret) this.secretStore.delete(pluginId, secretSettingKey(setting.id, setting.scope, normalizedScopeId));
else this.database.deleteSetting(pluginId, setting.id, setting.scope, normalizedScopeId);
// Clear the matching cloud-sync sidecar so collect does not rehydrate the
// retained remote value after an intentional local reset.
if (typeof this.database.deleteSyncSidecar === "function" && !setting.secret) {
const sidecarKey = `${setting.id}\0${setting.scope}\0${normalizedScopeId}`;
try {
this.database.deleteSyncSidecar(pluginId, "settings", sidecarKey);
} catch {}
}
this.#emitChange("setting-reset", pluginId);
try {
await this.runtimeSupervisor.notify(pluginId, "plugin.settings.changed", {
settingId: setting.id,
scope: setting.scope,
scopeId: normalizedScopeId,
source: "host",
});
} catch {}
return { restartRequired: setting.restartRequired === true };
}
#runtimeGetSetting(params, context) {
return this.getSetting(context.pluginId, params?.settingId, params?.scopeId).then((value) => ({
found: value !== undefined,
...(value === undefined ? {} : { value }),
}));
}
#runtimeUpdateSetting(params, context) {
return this.updateSetting(context.pluginId, params?.settingId, params?.value, params?.scopeId, { source: "plugin" });
}
#context(extra) {
return Object.assign(Object.create(null), extra ?? {}, Object.fromEntries(this.contextKeys));
}
snapshot(options = {}) {
const locale = options.locale ?? this.getLocale();
const context = this.#context(options.context);
const plugins = [];
for (const plugin of this.database.listPlugins()) {
if (!isContributionAvailable(plugin)) continue;
const contributes = plugin.manifest.contributes ?? {};
const localize = (value) => resolveLocalizedText(value, locale);
const commands = (contributes.commands ?? []).map((command) => ({
...command,
title: localize(command.title),
category: command.category == null ? undefined : localize(command.category),
description: command.description == null ? undefined : localize(command.description),
enabled: evaluateContextKeyExpression(command.enablement, context),
}));
const commandById = new Map(commands.map((command) => [command.id, command]));
const keybindings = (contributes.keybindings ?? []).map((keybinding) => ({
...keybinding,
enabled: evaluateContextKeyExpression(keybinding.when, context)
&& commandById.get(keybinding.command)?.enabled !== false,
}));
const keybindingByCommand = new Map();
for (const keybinding of keybindings) {
if (keybinding.enabled && !keybindingByCommand.has(keybinding.command)) {
keybindingByCommand.set(keybinding.command, keybinding);
}
}
const menus = (contributes.menus ?? []).map((menu, index) => {
const menuContext = this.#context(options.menuContexts?.[menu.location] ?? options.context);
const command = commandById.get(menu.command);
return {
...menu,
id: `${plugin.id}:menu:${index}`,
title: menu.title == null ? command?.title ?? menu.command : localize(menu.title),
visible: evaluateContextKeyExpression(menu.when, menuContext),
enabled: evaluateContextKeyExpression(menu.enablement, menuContext)
&& evaluateContextKeyExpression(command?.enablement, menuContext),
checked: menu.checked == null ? undefined : evaluateContextKeyExpression(menu.checked, menuContext),
shortcut: menu.showKeybinding === false
? undefined
: resolvePlatformKeybinding(keybindingByCommand.get(menu.command), options.platform),
};
});
const settings = (contributes.settings ?? []).map((setting) => {
const requestedScopeId = options.scopeIds?.[setting.scope];
const scopeId = setting.scope === "application"
? "application"
: setting.scope === "device"
? (requestedScopeId ?? "device")
: (requestedScopeId ?? null);
const stored = scopeId == null
? undefined
: setting.secret
? this.secretStore.getReference(plugin.id, secretSettingKey(setting.id, setting.scope, scopeId))
: this.database.getSetting(plugin.id, setting.id, setting.scope, scopeId);
return {
...setting,
label: localize(setting.label),
description: setting.description == null ? undefined : localize(setting.description),
placeholder: setting.placeholder == null ? undefined : localize(setting.placeholder),
options: setting.options?.map((option) => ({
...option,
label: localize(option.label),
description: option.description == null ? undefined : localize(option.description),
})),
visible: evaluateContextKeyExpression(setting.when, context),
configured: stored !== undefined && stored !== null,
value: setting.secret ? undefined : freezeJson(stored === undefined ? setting.default : stored),
scopeId,
};
});
const views = (contributes.views ?? []).map((view) => ({
...view,
title: localize(view.title),
visible: evaluateContextKeyExpression(view.when, context),
}));
plugins.push({
id: plugin.id,
version: plugin.activeVersion,
displayName: localize(plugin.manifest.displayName ?? plugin.manifest.name),
description: localize(plugin.manifest.description ?? ""),
commands,
keybindings,
menus,
settings,
views,
});
}
return freezeJson({ locale, plugins });
}
async setEnvironment(environment) {
assertJsonValue(environment, "environment");
this.environment = freezeJson(environment);
this.environmentRevision += 1;
for (const plugin of this.database.listPlugins()) {
if (!plugin.enabled || plugin.runtime.status !== "running") continue;
try {
await this.runtimeSupervisor.notify(plugin.id, "plugin.environment.changed", this.environment);
const identity = this.runtimeSupervisor.getRuntimeIdentity?.(plugin.id);
if (identity?.runtimeId) {
this.runtimeEnvironmentState.set(plugin.id, {
runtimeId: identity.runtimeId,
revision: this.environmentRevision,
});
}
} catch {}
}
this.#emitChange("environment");
}
}
module.exports = {
MAX_SETTING_BYTES,
MAX_PATTERN_VALUE_LENGTH,
PluginContributionService,
assertJsonValue,
assertSettingValue,
compileSettingPattern,
normalizeScopeId,
resolveLocalizedText,
resolvePlatformKeybinding,
secretSettingKey,
isContributionAvailable,
shouldActivateOnStartup,
};