pluginMarketplaceService.ts ×39

Frontier kind: Code frontier

unlabeled · c_ab6e7d11b9b9

178 tests · 29339 LOC · 151 files · introduces 0 tests · 915 LOC · 4 files

Introduces — evidence that enters the hierarchy at this concept

Code
57 ranges915 lines · 4 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
2777 ranges29339 lines · 151 files · Browse complete extent
All tests (intent)
178 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

4 files ranked by introduced lines: 915 introduced LOC across 57 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts 406 introduced LOC · 39 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- pluginMarketplaceService.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { runWhenGlobalIdle } from '../../../../../base/common/async.js';
7 > import { CancellationToken } from '../../../../../base/common/cancellation.js';
8 > import { Event } from '../../../../../base/common/event.js';
9 > import { parse as parseJSONC } from '../../../../../base/common/json.js';
10 > import { Lazy } from '../../../../../base/common/lazy.js';
11 > import { Disposable } from '../../../../../base/common/lifecycle.js';
12 > import { revive } from '../../../../../base/common/marshalling.js';
13 > import { autorun, derived, IObservable, observableFromEvent, observableValue } from '../../../../../base/common/observable.js';
14 > import { isEqual, isEqualOrParent, joinPath, normalizePath, relativePath } from '../../../../../base/common/resources.js';
15 > import { URI } from '../../../../../base/common/uri.js';
16 > import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
17 > import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
18 > import { IFileService } from '../../../../../platform/files/common/files.js';
19 > import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
20 > import { ILogService } from '../../../../../platform/log/common/log.js';
21 > import { ObservableMemento, observableMemento } from '../../../../../platform/observable/common/observableMemento.js';
22 > import { asJson, IRequestService } from '../../../../../platform/request/common/request.js';
23 > import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
24 > import type { Dto } from '../../../../services/extensions/common/proxyIdentifier.js';
25 > import { AutoUpdateConfigurationKey, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
26 > import { ChatConfiguration } from '../constants.js';
27 > import { IAgentPluginRepositoryService } from './agentPluginRepositoryService.js';
28 > import { FileBackedInstalledPluginsStore, IStoredInstalledPlugin } from './fileBackedInstalledPluginsStore.js';
29 > import { IWorkspacePluginSettingsService } from './workspacePluginSettingsService.js';
30 > import { IWorkspaceTrustManagementService } from '../../../../../platform/workspace/common/workspaceTrust.js';
31 > import { readAgentPluginManifest } from '../../../../../platform/agentPlugins/common/agentPluginParser.js';
32 > import { type IMarketplaceReference, deduplicateMarketplaceReferences, MarketplaceReferenceKind, parseMarketplaceObjectEntry, parseMarketplaceReference, parseMarketplaceReferences, readConfiguredMarketplaces } from './marketplaceReference.js';
33 > import { getStrictKnownMarketplaces, isMarketplaceReferenceAllowed } from './strictKnownMarketplaces.js';
34 >
35 > // Re-export marketplace reference types for downstream consumers.
36 > export { deduplicateMarketplaceReferences, extraKnownMarketplacesToConfigDict, MarketplaceReferenceKind, parseMarketplaceReference, parseMarketplaceReferences, readConfiguredMarketplaces } from './marketplaceReference.js';
37 > export type { IConfiguredMarketplaces, IMarketplaceReference } from './marketplaceReference.js';
38 >
39 > export const enum MarketplaceType {
40 > Copilot = 'copilot',
41 > Claude = 'claude',
42 > OpenPlugin = 'openPlugin',
43 > }
44 >
45 > export const enum PluginSourceKind {
46 > RelativePath = 'relativePath',
47 > GitHub = 'github',
48 > GitUrl = 'url',
49 > Npm = 'npm',
50 > Pip = 'pip',
51 > }
52 >
53 > export interface IRelativePathPluginSource {
54 > readonly kind: PluginSourceKind.RelativePath;
55 > /** Resolved relative path within the marketplace repository. */
56 > readonly path: string;
57 > }
58 >
59 > export interface IGitHubPluginSource {
60 > readonly kind: PluginSourceKind.GitHub;
61 > readonly repo: string;
62 > readonly ref?: string;
63 > readonly sha?: string;
64 > readonly path?: string;
65 > }
66 >
67 > export interface IGitUrlPluginSource {
68 > readonly kind: PluginSourceKind.GitUrl;
69 > /** Full git repository URL (must end with .git). */
70 > readonly url: string;
71 > readonly ref?: string;
72 > readonly sha?: string;
73 > /** Subdirectory within the repository where the plugin lives (for `git-subdir` sources). */
74 > readonly path?: string;
75 > }
76 >
77 > export interface INpmPluginSource {
78 > readonly kind: PluginSourceKind.Npm;
79 > readonly package: string;
80 > readonly version?: string;
81 > readonly registry?: string;
82 > }
83 >
84 > export interface IPipPluginSource {
85 > readonly kind: PluginSourceKind.Pip;
86 > readonly package: string;
87 > readonly version?: string;
88 > readonly registry?: string;
89 > }
90 >
91 > export type IPluginSourceDescriptor =
92 > | IRelativePathPluginSource
93 > | IGitHubPluginSource
94 > | IGitUrlPluginSource
95 > | INpmPluginSource
96 > | IPipPluginSource;
97 >
98 > export interface IMarketplacePlugin {
99 > readonly name: string;
100 > readonly description: string;
101 > readonly version: string;
102 > /** Subdirectory within the repository where the plugin lives (for relative-path sources). */
103 > readonly source: string;
104 > /** Structured source descriptor indicating how the plugin should be fetched/installed. */
105 > readonly sourceDescriptor: IPluginSourceDescriptor;
106 > /** Marketplace label shown in UI and plugin provenance. */
107 > readonly marketplace: string;
108 > /** Canonical reference for clone/update/install location resolution. */
109 > readonly marketplaceReference: IMarketplaceReference;
110 > /** The type of marketplace this plugin comes from. */
111 > readonly marketplaceType: MarketplaceType;
112 > readonly readmeUri?: URI;
113 > }
114 >
115 > /** Raw JSON shape of a remote plugin source object in marketplace.json. */
116 > interface IJsonPluginSource {
117 > readonly source: string;
118 > readonly repo?: string;
119 > readonly url?: string;
120 > readonly package?: string;
121 > readonly ref?: string;
122 > readonly sha?: string;
123 > readonly path?: string;
124 > readonly version?: string;
125 > readonly registry?: string;
126 > }
127 >
128 > interface IMarketplaceJson {
129 > readonly metadata?: {
130 > readonly pluginRoot?: string;
131 > };
132 > readonly plugins?: readonly {
133 > readonly name?: string;
134 > readonly description?: string;
135 > readonly version?: string;
136 > readonly source?: string | IJsonPluginSource;
137 > }[];
138 > }
139 >
140 > export interface IMarketplaceInstalledPlugin {
141 > readonly pluginUri: URI;
142 > readonly plugin: IMarketplacePlugin;
143 > }
144 >
145 > export const IPluginMarketplaceService = createDecorator<IPluginMarketplaceService>('pluginMarketplaceService');
146 >
147 > export interface IPluginMarketplaceService {
148 > readonly _serviceBrand: undefined;
149 > readonly onDidChangeMarketplaces: Event<void>;
150 > /** Installed marketplace plugins, backed by storage. */
151 > readonly installedPlugins: IObservable<readonly IMarketplaceInstalledPlugin[]>;
152 > /**
153 > * Observable that is `true` when at least one cloned marketplace
154 > * repository has upstream changes available. Checked periodically
155 > * (approximately once per day) when `extensions.autoUpdate` is enabled.
156 > */
157 > readonly hasUpdatesAvailable: IObservable<boolean>;
158 > /**
159 > * Observable snapshot of the last {@link fetchMarketplacePlugins} result.
160 > * Empty until the first fetch completes. Views should use this for
161 > * synchronous outdated-detection instead of calling fetchMarketplacePlugins.
162 > */
163 > readonly lastFetchedPlugins: IObservable<readonly IMarketplacePlugin[]>;
164 > /**
165 > * Set of recommended plugin keys (`"pluginName@marketplaceName"`) aggregated
166 > * from workspace-defined settings (e.g. `.claude/settings.json`). Providers
167 > * may be added over time; consumers should not assume a specific source.
168 > */
169 > readonly recommendedPlugins: IObservable<ReadonlySet<string>>;
170 > /** Resets {@link hasUpdatesAvailable} to `false`. */
171 > clearUpdatesAvailable(): void;
172 > fetchMarketplacePlugins(token: CancellationToken): Promise<IMarketplacePlugin[]>;
173 > getMarketplacePluginMetadata(pluginUri: URI): IMarketplacePlugin | undefined;
174 > addInstalledPlugin(pluginUri: URI, plugin: IMarketplacePlugin): void;
175 > removeInstalledPlugin(pluginUri: URI): void;
176 > /** Returns whether the given marketplace is trusted — either explicitly trusted by the user, or allowed by the enterprise allowlist when strict mode is active. */
177 > isMarketplaceTrusted(ref: IMarketplaceReference): boolean;
178 > /**
179 > * Returns whether the strict-marketplace enterprise policy
180 > * (`chat.plugins.strictMarketplaces`) is active — i.e. an allowlist is
181 > * configured. When active, blocked marketplaces cannot be trusted by the user.
182 > */
183 > isStrictMarketplacePolicyActive(): boolean;
184 > /** Records that the user trusts the given marketplace, persisted permanently. */
185 > trustMarketplace(ref: IMarketplaceReference): void;
186 > /**
187 > * Reads marketplace definition files from an already-cloned repository
188 > * directory and returns the declared plugins. Used by direct-install flows
189 > * that clone a repo first, then need to discover its plugins.
190 > */
191 > readPluginsFromDirectory(repoDir: URI, reference: IMarketplaceReference): Promise<IMarketplacePlugin[]>;
192 > /**
193 > * Reads a single-plugin manifest (e.g. `.claude-plugin/plugin.json`) at the
194 > * root of an already-cloned repository directory and returns a synthesised
195 > * {@link IMarketplacePlugin} describing the repository as a single plugin.
196 > * Used by direct-install flows when {@link readPluginsFromDirectory} finds
197 > * no marketplace index.
198 > *
199 > * Returns `undefined` when no recognised manifest is present at the repo
200 > * root.
201 > */
202 > readSinglePluginManifest(repoDir: URI, reference: IMarketplaceReference): Promise<IMarketplacePlugin | undefined>;
203 > /**
204 > * Returns whether the given directory is a standalone plugin — i.e. it
205 > * contains a single-plugin manifest (e.g. `.plugin/plugin.json`,
206 > * `.claude-plugin/plugin.json`, or `plugin.json`) at its root but is not a
207 > * marketplace. Used by direct-install flows to route a local folder to the
208 > * appropriate configuration.
209 > */
210 > isPluginDirectory(repoDir: URI): Promise<boolean>;
211 > }
212 >
213 > /**
214 > * Marketplace definition files by type, checked in order per repository.
215 > * The first match determines the marketplace type.
216 > */
217 > const MARKETPLACE_DEFINITIONS: { type: MarketplaceType; path: string }[] = [
218 > { type: MarketplaceType.OpenPlugin, path: 'marketplace.json' },
219 > { type: MarketplaceType.OpenPlugin, path: '.plugin/marketplace.json' },
220 > { type: MarketplaceType.Copilot, path: '.github/plugin/marketplace.json' },
221 > { type: MarketplaceType.Claude, path: '.claude-plugin/marketplace.json' },
222 > ];
223 >
224 > /**
225 > * Single-plugin manifest files by type, checked in order. Used when a cloned
226 > * source repository has no marketplace index — the repository itself is the
227 > * plugin. Order matches {@link detectPluginFormat} so that runtime format
228 > * detection later agrees with the marketplace type chosen here.
229 > */
230 > const SINGLE_PLUGIN_MANIFEST_DEFINITIONS: { type: MarketplaceType; path: string }[] = [
231 > { type: MarketplaceType.OpenPlugin, path: '.plugin/plugin.json' },
232 > { type: MarketplaceType.Claude, path: '.claude-plugin/plugin.json' },
233 > { type: MarketplaceType.Copilot, path: 'plugin.json' },
234 > ];
235 >
236 > const GITHUB_MARKETPLACE_CACHE_TTL_MS = 8 * 60 * 60 * 1000;
237 > const GITHUB_MARKETPLACE_CACHE_STORAGE_KEY = 'chat.plugins.marketplaces.githubCache.v1';
238 >
239 > /** Interval between periodic plugin update checks (24 hours). */
240 > const PLUGIN_UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
241 >
242 > const PLUGIN_UPDATE_LAST_CHECK_STORAGE_KEY = 'chat.plugins.lastUpdateCheck.v1';
243 >
244 > interface IGitHubMarketplaceCacheEntry {
245 > readonly plugins: readonly IMarketplacePlugin[];
246 > readonly expiresAt: number;
247 > readonly referenceRawValue: string;
248 > }
249 >
250 > type IStoredGitHubMarketplaceCache = Dto<Record<string, IGitHubMarketplaceCacheEntry>>;
251 >
252 > /**
253 > * Ensures that an {@link IMarketplacePlugin} loaded from storage has a
254 > * {@link IMarketplacePlugin.sourceDescriptor sourceDescriptor}. Plugins
255 > * persisted before the sourceDescriptor field was introduced will only
256 > * have the legacy `source` string — this function synthesises a
257 > * {@link PluginSourceKind.RelativePath} descriptor from it.
258 > */
259 function ensureSourceDescriptor(plugin: IMarketplacePlugin): IMarketplacePlugin {
260 if (plugin.sourceDescriptor) {
266 };
267 }
269 > const trustedMarketplacesMemento = observableMemento<readonly string[]>({
270 > defaultValue: [],
271 > key: 'chat.plugins.trustedMarketplaces.v1',
272 > toStorage: value => JSON.stringify(value),
273 > fromStorage: value => {
274 const parsed = JSON.parse(value);
275 return Array.isArray(parsed) ? parsed : [];
276 },
278 >
279 > interface IStoredLastFetchedPlugins {
280 > readonly plugins: readonly IMarketplacePlugin[];
281 > readonly fetchedAt: number;
282 > }
283 >
284 > const lastFetchedPluginsMemento = observableMemento<IStoredLastFetchedPlugins>({
285 > defaultValue: { plugins: [], fetchedAt: 0 },
286 > key: 'chat.plugins.lastFetchedPlugins.v2',
287 > toStorage: value => JSON.stringify(value),
288 > fromStorage: value => {
289 const parsed = JSON.parse(value);
290 if (parsed && Array.isArray(parsed.plugins)) {
293 return { plugins: [], fetchedAt: 0 };
294 },
296 >
297 > export class PluginMarketplaceService extends Disposable implements IPluginMarketplaceService {
298 > declare readonly _serviceBrand: undefined;
299 > private readonly _gitHubMarketplaceCache = new Lazy<Map<string, IGitHubMarketplaceCacheEntry>>(() => this._loadPersistedGitHubMarketplaceCache());
300 > private readonly _installedPluginsStore: FileBackedInstalledPluginsStore;
301 > private readonly _pluginMetadata = new Map<string, IMarketplacePlugin>();
302 > private readonly _trustedMarketplacesStore: ObservableMemento<readonly string[]>;
303 > private readonly _lastFetchedPluginsStore: ObservableMemento<IStoredLastFetchedPlugins>;
304 > private readonly _hasUpdatesAvailable = observableValue<boolean>('hasUpdatesAvailable', false);
305 > private _updateCheckTimer: ReturnType<typeof setTimeout> | undefined;
306 >
307 > readonly onDidChangeMarketplaces: Event<void>;
308 >
309 > readonly installedPlugins: IObservable<readonly IMarketplaceInstalledPlugin[]>;
310 > readonly hasUpdatesAvailable: IObservable<boolean> = this._hasUpdatesAvailable;
311 > readonly lastFetchedPlugins: IObservable<readonly IMarketplacePlugin[]>;
312 > readonly recommendedPlugins: IObservable<ReadonlySet<string>>;
313 >
314 > constructor(
315 @IConfigurationService private readonly _configurationService: IConfigurationService,
316 @IRequestService private readonly _requestService: IRequestService,
412 }));
413 }
415 > override dispose(): void {
416 if (this._updateCheckTimer !== undefined) {
417 clearTimeout(this._updateCheckTimer);
420 super.dispose();
421 }
423 > clearUpdatesAvailable(): void {
424 this._hasUpdatesAvailable.set(false, undefined);
425 }
427 > async fetchMarketplacePlugins(token: CancellationToken): Promise<IMarketplacePlugin[]> {
428 if (!this._configurationService.getValue<boolean>(ChatConfiguration.PluginsEnabled)) {
429 return [];
469 return plugins;
470 }
472 > private async _fetchFromGitHubRepo(reference: IMarketplaceReference, repo: string, token: CancellationToken): Promise<IMarketplacePlugin[]> {
473 const cache = this._gitHubMarketplaceCache.value;
474
523 return [];
524 }
526 > private _getCachedGitHubMarketplacePlugins(cache: Map<string, IGitHubMarketplaceCacheEntry>, cacheKey: string): IMarketplacePlugin[] | undefined {
527 const cached = cache.get(cacheKey);
528 if (!cached) {
538 return [...cached.plugins];
539 }
541 > private _loadPersistedGitHubMarketplaceCache(): Map<string, IGitHubMarketplaceCacheEntry> {
542 const cache = new Map<string, IGitHubMarketplaceCacheEntry>();
543 const now = Date.now();
574 return cache;
575 }
577 > private _savePersistedGitHubMarketplaceCache(cache: Map<string, IGitHubMarketplaceCacheEntry>): void {
578 const serialized: IStoredGitHubMarketplaceCache = {};
579 for (const [cacheKey, entry] of cache) {
601 );
602 }
604 > getMarketplacePluginMetadata(pluginUri: URI): IMarketplacePlugin | undefined {
605 return this._pluginMetadata.get(pluginUri.toString())
606 ?? [...this._pluginMetadata.entries()].find(([key]) => isEqualOrParent(pluginUri, URI.parse(key)))?.[1];
607 }
609 > addInstalledPlugin(pluginUri: URI, plugin: IMarketplacePlugin): void {
610 this._pluginMetadata.set(pluginUri.toString(), plugin);
611 const entry: IStoredInstalledPlugin = {
623 }
624 }
626 > removeInstalledPlugin(pluginUri: URI): void {
627 this._pluginMetadata.delete(pluginUri.toString());
628 const current = this._installedPluginsStore.get();
629 this._installedPluginsStore.set(current.filter(e => !isEqual(e.pluginUri, pluginUri)), undefined);
630 }
632 > isMarketplaceTrusted(ref: IMarketplaceReference): boolean {
633 // In strict mode (`chat.plugins.strictMarketplaces`, typically delivered via the
634 // `ChatStrictMarketplaces` enterprise policy), trust is governed entirely by the
642 return this._trustedMarketplacesStore.get().includes(ref.canonicalId);
643 }
645 > isStrictMarketplacePolicyActive(): boolean {
646 return getStrictKnownMarketplaces(this._configurationService.getValue(ChatConfiguration.StrictMarketplaces)) !== undefined;
647 }
649 > // --- Plugin metadata hydration -----------------------------------------------
650 >
651 > /**
652 > * Hydrates installed entries from marketplace metadata. Entries written
653 > * by current builds include the marketplace plugin name, which is enough
654 > * to re-read the full plugin descriptor from the marketplace source. Old
655 > * entries without a name fall back to matching by install URI.
656 > *
657 > * After hydration completes the installed-plugins store is "touched" so
658 > * that the derived {@link installedPlugins} observable re-evaluates with
659 > * the newly available metadata.
660 > */
661 > private async _hydratePluginMetadata(entries: readonly IStoredInstalledPlugin[]): Promise<void> {
662 let hydrated = 0;
663
693 }
694 }
696 > private async _readPluginsForInstalledEntry(reference: IMarketplaceReference, token: CancellationToken): Promise<IMarketplacePlugin[]> {
697 if (reference.kind === MarketplaceReferenceKind.GitHubShorthand && reference.githubRepo) {
698 return this._fetchFromGitHubRepo(reference, reference.githubRepo, token);
712 return plugins;
713 }
715 > /**
716 > * Shared logic to parse a marketplace.json into {@link IMarketplacePlugin}
717 > * objects. Used by both fetch and hydration paths.
718 > */
719 > private _parseMarketplacePlugins(json: IMarketplaceJson, reference: IMarketplaceReference, marketplaceType: MarketplaceType, repoDir?: URI): IMarketplacePlugin[] {
720 if (!json.plugins || !Array.isArray(json.plugins)) {
721 return [];
751 });
752 }
754 > trustMarketplace(ref: IMarketplaceReference): void {
755 const current = this._trustedMarketplacesStore.get();
756 if (!current.includes(ref.canonicalId)) {
758 }
759 }
761 > // --- Periodic update check ------------------------------------------------
762 >
763 > private _isAutoUpdateEnabled(): boolean {
764 return this._extensionsWorkbenchService.getAutoUpdateValue() !== 'off';
765 }
767 > /**
768 > * (Re-)schedules the next periodic update check. Called on
769 > * construction and whenever the auto-update config changes.
770 > */
771 > private _scheduleUpdateCheck(): void {
772 if (this._updateCheckTimer !== undefined) {
773 clearTimeout(this._updateCheckTimer);
789 this._updateCheckTimer = setTimeout(() => this._runUpdateCheck(), delay);
790 }
792 > private async _runUpdateCheck(): Promise<void> {
793 this._updateCheckTimer = undefined;
794
836 }
837 }
839 > private async _fetchFromClonedRepo(reference: IMarketplaceReference, token: CancellationToken): Promise<IMarketplacePlugin[]> {
840 let repoDir: URI;
841 try {
848 return this._readPluginsFromDirectory(repoDir, reference, token);
849 }
851 > async readPluginsFromDirectory(repoDir: URI, reference: IMarketplaceReference): Promise<IMarketplacePlugin[]> {
852 return this._readPluginsFromDirectory(repoDir, reference);
853 }
855 > async readSinglePluginManifest(repoDir: URI, reference: IMarketplaceReference): Promise<IMarketplacePlugin | undefined> {
856 // Single-plugin repos are only meaningful for direct git clones —
857 // there's no synthetic relative-path source to fall back on.
912 return undefined;
913 }
915 > async isPluginDirectory(repoDir: URI): Promise<boolean> {
916 if (await readAgentPluginManifest(repoDir, this._fileService)) {
917 return true;
924 return false;
925 }
927 > private async _readPluginsFromDirectory(repoDir: URI, reference: IMarketplaceReference, token?: CancellationToken): Promise<IMarketplacePlugin[]> {
928 return this._readPluginsFromDefinitions(reference, async (defPath) => {
929 if (token?.isCancellationRequested) {
939 }, repoDir);
940 }
942 > /**
943 > * Iterates over {@link MARKETPLACE_DEFINITIONS} paths, calling
944 > * {@link readJson} for each to obtain the parsed JSON. Returns the
945 > * plugins from the first definition that yields a valid result.
946 > */
947 > private async _readPluginsFromDefinitions(
948 reference: IMarketplaceReference,
949 readJson: (defPath: string) => Promise<IMarketplaceJson | undefined>,
961 return [];
962 }
964 >
965 function normalizeMarketplacePath(value: string): string {
966 let normalized = value.trim().replace(/\\/g, '/');
968 return normalized;
969 }
971 > /**
972 > * Resolve plugin source from marketplace metadata.
973 > * - If pluginRoot exists, plugin source is resolved relative to it.
974 > * - If source already includes pluginRoot, it's preserved.
975 > * Validation of whether the final path is allowed is performed by the install service.
976 > */
977 function resolvePluginSource(pluginRoot: string | undefined, source: string): string | undefined {
978 const normalizedRoot = pluginRoot ? normalizeMarketplacePath(pluginRoot) : '';
988 return relativePath(repoRoot, resolvedUri) ?? undefined;
989 }
991 > /**
992 > * Parse a raw `source` field from marketplace.json into a structured
993 > * {@link IPluginSourceDescriptor}. Accepts either a relative-path string
994 > * or a JSON object with a `source` discriminant indicating the kind.
995 > */
996 > export function parsePluginSource(
997 rawSource: string | IJsonPluginSource | undefined,
998 pluginRoot: string | undefined,
1125 }
1126 }
1128 function isOptionalString(value: unknown): value is string | undefined {
1129 return value === undefined || typeof value === 'string';
1130 }
1132 function isOptionalGitSha(value: unknown): value is string | undefined {
1133 return value === undefined || (typeof value === 'string' && /^[0-9a-fA-F]{40}$/.test(value));
1134 }
1136 function isValidGitHubRepo(repo: string): boolean {
1137 return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo);
1138 }
1140 > /**
1141 > * Returns a human-readable label for a plugin source descriptor,
1142 > * suitable for error messages and UI display.
1143 > */
1144 > export function getPluginSourceLabel(descriptor: IPluginSourceDescriptor): string {
1145 switch (descriptor.kind) {
1146 case PluginSourceKind.RelativePath:
1156 }
1157 }
1159 > /**
1160 > * Returns `true` when the marketplace source descriptor differs from the
1161 > * installed one — meaning an update should be performed.
1162 > */
1163 > export function hasSourceChanged(installed: IPluginSourceDescriptor, marketplace: IPluginSourceDescriptor): boolean {
1164 if (installed.kind !== marketplace.kind) {
1165 return true;
1183 }
1184 }
1186 function getMarketplaceReadmeUri(repo: string, source: string): URI {
1187 const normalizedSource = source.trim().replace(/^\.?\/+|\/+$/g, '');
1189 return URI.parse(`https://github.com/${repo}/blob/main/${readmePath}`);
1190 }
1192 function getMarketplaceReadmeFileUri(repoDir: URI, source: string): URI {
1193 const normalizedSource = source.trim().replace(/^\.?\/+|\/+$/g, '');
src/vs/workbench/contrib/extensions/common/extensions.ts 259 introduced LOC · 4 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- extensions.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
7 > import { Event } from '../../../../base/common/event.js';
8 > import { IPager } from '../../../../base/common/paging.js';
9 > import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, InstallExtensionResult, InstallOptions } from '../../../../platform/extensionManagement/common/extensionManagement.js';
10 > import { EnablementState, IExtensionManagementServer, IResourceExtension } from '../../../services/extensionManagement/common/extensionManagement.js';
11 > import { CancellationToken } from '../../../../base/common/cancellation.js';
12 > import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
13 > import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
14 > import { IExtensionManifest, ExtensionType } from '../../../../platform/extensions/common/extensions.js';
15 > import { URI } from '../../../../base/common/uri.js';
16 > import { IView, IViewPaneContainer } from '../../../common/views.js';
17 > import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
18 > import { IExtensionsStatus as IExtensionRuntimeStatus } from '../../../services/extensions/common/extensions.js';
19 > import { IExtensionEditorOptions } from './extensionsInput.js';
20 > import { MenuId } from '../../../../platform/actions/common/actions.js';
21 > import { ProgressLocation } from '../../../../platform/progress/common/progress.js';
22 > import { Severity } from '../../../../platform/notification/common/notification.js';
23 > import { IMarkdownString } from '../../../../base/common/htmlContent.js';
24 > import { localize2 } from '../../../../nls.js';
25 > import { ExtensionGalleryManifestStatus } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
26 >
27 > export const VIEWLET_ID = 'workbench.view.extensions';
28 > export const EXTENSIONS_CATEGORY = localize2('extensions', "Extensions");
29 >
30 > export interface IExtensionsViewPaneContainer extends IViewPaneContainer {
31 > readonly searchValue: string | undefined;
32 > search(text: string): void;
33 > refresh(): Promise<void>;
34 > }
35 >
36 > export interface IWorkspaceRecommendedExtensionsView extends IView {
37 > installWorkspaceRecommendations(): Promise<void>;
38 > }
39 >
40 > export const enum ExtensionState {
41 > Installing,
42 > Installed,
43 > Uninstalling,
44 > Uninstalled
45 > }
46 >
47 > export const enum ExtensionRuntimeActionType {
48 > ReloadWindow = 'reloadWindow',
49 > RestartExtensions = 'restartExtensions',
50 > DownloadUpdate = 'downloadUpdate',
51 > ApplyUpdate = 'applyUpdate',
52 > QuitAndInstall = 'quitAndInstall',
53 > }
54 >
55 > export type ExtensionRuntimeState = { action: ExtensionRuntimeActionType; reason: string };
56 >
57 > export interface IExtension {
58 > readonly type: ExtensionType;
59 > readonly isBuiltin: boolean;
60 > readonly isWorkspaceScoped: boolean;
61 > readonly state: ExtensionState;
62 > readonly name: string;
63 > readonly displayName: string;
64 > readonly identifier: IExtensionIdentifier;
65 > readonly publisher: string;
66 > readonly publisherDisplayName: string;
67 > readonly publisherUrl?: URI;
68 > readonly publisherDomain?: { link: string; verified: boolean };
69 > readonly publisherSponsorLink?: URI;
70 > readonly pinned: boolean;
71 > readonly version: string;
72 > readonly private: boolean;
73 > readonly latestVersion: string;
74 > readonly preRelease: boolean;
75 > readonly isPreReleaseVersion: boolean;
76 > readonly hasPreReleaseVersion: boolean;
77 > readonly hasReleaseVersion: boolean;
78 > readonly description: string;
79 > readonly url?: string;
80 > readonly repository?: string;
81 > readonly supportUrl?: string;
82 > readonly iconUrl?: string;
83 > readonly iconUrlFallback?: string;
84 > readonly licenseUrl?: string;
85 > readonly installCount?: number;
86 > readonly rating?: number;
87 > readonly ratingCount?: number;
88 > readonly ratingUrl?: string;
89 > readonly outdated: boolean;
90 > readonly outdatedTargetPlatform: boolean;
91 > readonly runtimeState: ExtensionRuntimeState | undefined;
92 > readonly enablementState: EnablementState;
93 > readonly tags: readonly string[];
94 > readonly categories: readonly string[];
95 > readonly dependencies: string[];
96 > readonly extensionPack: string[];
97 > readonly telemetryData: any;
98 > readonly preview: boolean;
99 > getManifest(token: CancellationToken): Promise<IExtensionManifest | null>;
100 > hasReadme(): boolean;
101 > getReadme(token: CancellationToken): Promise<string>;
102 > hasChangelog(): boolean;
103 > getChangelog(token: CancellationToken): Promise<string>;
104 > readonly server?: IExtensionManagementServer;
105 > readonly local?: ILocalExtension;
106 > gallery?: IGalleryExtension;
107 > readonly resourceExtension?: IResourceExtension;
108 > readonly isMalicious: boolean | undefined;
109 > readonly maliciousInfoLink: string | undefined;
110 > readonly deprecationInfo?: IDeprecationInfo;
111 > readonly missingFromGallery?: boolean;
112 > }
113 >
114 > export const IExtensionsWorkbenchService = createDecorator<IExtensionsWorkbenchService>('extensionsWorkbenchService');
115 >
116 > export interface InstallExtensionOptions extends InstallOptions {
117 > version?: string;
118 > justification?: string | { reason: string; action: string };
119 > enable?: boolean;
120 > installEverywhere?: boolean;
121 > }
122 >
123 > export interface IExtensionsNotification {
124 > readonly message: string | IMarkdownString;
125 > readonly severity: Severity;
126 > readonly extensions: IExtension[];
127 > readonly query?: string;
128 > readonly action?: { readonly label: string; run(): void };
129 > dismiss?(): void;
130 > }
131 >
132 > export interface IExtensionsWorkbenchService {
133 > readonly _serviceBrand: undefined;
134 > readonly onChange: Event<IExtension | undefined>;
135 > readonly onReset: Event<void>;
136 > readonly local: IExtension[];
137 > readonly installed: IExtension[];
138 > readonly outdated: IExtension[];
139 > readonly whenInitialized: Promise<void>;
140 > queryLocal(server?: IExtensionManagementServer): Promise<IExtension[]>;
141 > queryGallery(token: CancellationToken): Promise<IPager<IExtension>>;
142 > queryGallery(options: IQueryOptions, token: CancellationToken): Promise<IPager<IExtension>>;
143 > getExtensions(extensionInfos: IExtensionInfo[], token: CancellationToken): Promise<IExtension[]>;
144 > getExtensions(extensionInfos: IExtensionInfo[], options: IExtensionQueryOptions, token: CancellationToken): Promise<IExtension[]>;
145 > getResourceExtensions(locations: URI[], isWorkspaceScoped: boolean): Promise<IExtension[]>;
146 > canInstall(extension: IExtension): Promise<true | IMarkdownString>;
147 > install(id: string, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise<IExtension>;
148 > install(vsix: URI, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise<IExtension>;
149 > install(extension: IExtension, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise<IExtension>;
150 > installInServer(extension: IExtension, server: IExtensionManagementServer, installOptions?: InstallOptions): Promise<void>;
151 > downloadVSIX(extension: string, versionKind: 'prerelease' | 'release' | 'any'): Promise<void>;
152 > uninstall(extension: IExtension): Promise<void>;
153 > togglePreRelease(extension: IExtension): Promise<void>;
154 > canSetLanguage(extension: IExtension): boolean;
155 > setLanguage(extension: IExtension): Promise<void>;
156 > setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise<void>;
157 > isAutoUpdateEnabledFor(extensionOrPublisher: IExtension | string): boolean;
158 > updateAutoUpdateEnablementFor(extensionOrPublisher: IExtension | string, enable: boolean): Promise<void>;
159 > shouldRequireConsentToUpdate(extension: IExtension): Promise<string | undefined>;
160 > updateAutoUpdateForAllExtensions(value: boolean): Promise<void>;
161 > open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise<void>;
162 > openSearch(searchValue: string, focus?: boolean): Promise<void>;
163 > getAutoUpdateValue(): AutoUpdateConfigurationValue;
164 > isAutoUpdateDelayed(extension: IExtension): boolean;
165 > getAutoUpdateDelayRemaining(extension: IExtension): number;
166 > getAutoUpdateDelay(): number;
167 > checkForUpdates(): Promise<void>;
168 > getExtensionRuntimeStatus(extension: IExtension): IExtensionRuntimeStatus | undefined;
169 > updateAll(): Promise<InstallExtensionResult[]>;
170 > updateRunningExtensions(message?: string): Promise<void>;
171 >
172 > readonly onDidChangeExtensionsNotification: Event<IExtensionsNotification | undefined>;
173 > getExtensionsNotification(): IExtensionsNotification | undefined;
174 >
175 > // Sync APIs
176 > isExtensionIgnoredToSync(extension: IExtension): boolean;
177 > toggleExtensionIgnoredToSync(extension: IExtension): Promise<void>;
178 > toggleApplyExtensionToAllProfiles(extension: IExtension): Promise<void>;
179 > }
180 >
181 > export const enum ExtensionEditorTab {
182 > Readme = 'readme',
183 > Features = 'features',
184 > Changelog = 'changelog',
185 > Dependencies = 'dependencies',
186 > ExtensionPack = 'extensionPack',
187 > }
188 >
189 > export const ConfigurationKey = 'extensions';
190 > export const AutoUpdateConfigurationKey = 'extensions.autoUpdate';
191 > export const AutoUpdateDelayConfigurationKey = 'extensions.autoUpdateDelay';
192 > export const AutoCheckUpdatesConfigurationKey = 'extensions.autoCheckUpdates';
193 > export const CloseExtensionDetailsOnViewChangeKey = 'extensions.closeExtensionDetailsOnViewChange';
194 > export const AutoRestartConfigurationKey = 'extensions.autoRestart';
195 >
196 > export type AutoUpdateConfigurationValue = 'on' | 'off';
197 >
198 > export interface IExtensionsConfiguration {
199 > autoUpdate: AutoUpdateConfigurationValue;
200 > autoUpdateDelay: number;
201 > autoCheckUpdates: boolean;
202 > ignoreRecommendations: boolean;
203 > closeExtensionDetailsOnViewChange: boolean;
204 > }
205 >
206 > export interface IExtensionContainer extends IDisposable {
207 > extension: IExtension | null;
208 > updateWhenCounterExtensionChanges?: boolean;
209 > update(): void;
210 > }
211 >
212 > export interface IExtensionsViewState {
213 > readonly onFocus: Event<IExtension>;
214 > readonly onBlur: Event<IExtension>;
215 > filters: {
216 > featureId?: string;
217 > };
218 > }
219 >
220 > export class ExtensionContainers extends Disposable {
221 >
222 > constructor(
223 private readonly containers: IExtensionContainer[],
224 @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService
227 this._register(extensionsWorkbenchService.onChange(this.update, this));
228 }
230 > set extension(extension: IExtension) {
231 this.containers.forEach(c => c.extension = extension);
232 }
234 > private update(extension: IExtension | undefined): void {
235 for (const container of this.containers) {
236 if (extension && container.extension) {
249 }
250 }
251 > } extensions.ts
252 >
253 > export const WORKSPACE_RECOMMENDATIONS_VIEW_ID = 'workbench.views.extensions.workspaceRecommendations';
254 > export const OUTDATED_EXTENSIONS_VIEW_ID = 'workbench.views.extensions.searchOutdated';
255 > export const TOGGLE_IGNORE_EXTENSION_ACTION_ID = 'workbench.extensions.action.toggleIgnoreExtension';
256 > export const SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID = 'workbench.extensions.action.installVSIX';
257 > export const INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID = 'workbench.extensions.command.installFromVSIX';
258 >
259 > export const LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID = 'workbench.extensions.action.listWorkspaceUnsupportedExtensions';
260 >
261 > // Context Keys
262 > export const DefaultViewsContext = new RawContextKey<boolean>('defaultExtensionViews', true);
263 > export const HasOutdatedExtensionsContext = new RawContextKey<boolean>('hasOutdatedExtensions', false);
264 > export const CONTEXT_HAS_GALLERY = new RawContextKey<boolean>('hasGallery', false);
265 > export const CONTEXT_EXTENSIONS_GALLERY_STATUS = new RawContextKey<string>('extensionsGalleryStatus', ExtensionGalleryManifestStatus.Unavailable);
266 > export const ExtensionResultsListFocused = new RawContextKey<boolean>('extensionResultListFocused ', true);
267 > export const SearchMcpServersContext = new RawContextKey<boolean>('searchMcpServers', false);
268 > export const SearchAgentPluginsContext = new RawContextKey<boolean>('searchAgentPlugins', false);
269 >
270 > // Context Menu Groups
271 > export const THEME_ACTIONS_GROUP = '_theme_';
272 > export const INSTALL_ACTIONS_GROUP = '0_install';
273 > export const UPDATE_ACTIONS_GROUP = '0_update';
274 >
275 > export const extensionsSearchActionsMenu = new MenuId('extensionsSearchActionsMenu');
276 > export const extensionsFilterSubMenu = new MenuId('extensionsFilterSubMenu');
277 >
278 > export interface IExtensionArg {
279 > id: string;
280 > version: string;
281 > location: URI | undefined;
282 > galleryLink: string | undefined;
283 > }
src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts 127 introduced LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentPluginRepositoryService.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { URI } from '../../../../../base/common/uri.js';
7 > import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
8 > import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceType, PluginSourceKind } from './pluginMarketplaceService.js';
9 > import { IPluginSource } from './pluginSource.js';
10 >
11 > export const IAgentPluginRepositoryService = createDecorator<IAgentPluginRepositoryService>('agentPluginRepositoryService');
12 >
13 > /**
14 > * Options for ensuring a marketplace repository is available locally.
15 > */
16 > export interface IEnsureRepositoryOptions {
17 > /** Optional progress notification title shown during clone. */
18 > readonly progressTitle?: string;
19 > /** Label used in clone failure messaging. */
20 > readonly failureLabel?: string;
21 > /** Marketplace type metadata to persist in the marketplace index. */
22 > readonly marketplaceType?: MarketplaceType;
23 > }
24 >
25 > /**
26 > * Options for pulling the latest changes from a cloned marketplace repository.
27 > */
28 > export interface IPullRepositoryOptions {
29 > /** Optional plugin name used in progress messaging. */
30 > readonly pluginName?: string;
31 > /** Label used in pull failure messaging. */
32 > readonly failureLabel?: string;
33 > /** Marketplace type metadata for repository index updates. */
34 > readonly marketplaceType?: MarketplaceType;
35 > /** When `true`, suppresses progress notifications. */
36 > readonly silent?: boolean;
37 > }
38 >
39 > /**
40 > * Manages cloning, cache location resolution, and update operations for
41 > * agent plugin marketplace repositories.
42 > */
43 > export interface IAgentPluginRepositoryService {
44 > readonly _serviceBrand: undefined;
45 >
46 > /**
47 > * Root directory where agent plugins are stored on disk.
48 > * On native this is `~/{dataFolderName}/agent-plugins/`; on web it
49 > * falls back to `{cacheHome}/agentPlugins/`.
50 > */
51 > readonly agentPluginsHome: URI;
52 >
53 > /**
54 > * Returns the local cache URI for a marketplace repository reference.
55 > * Uses a storage-backed marketplace index when available.
56 > */
57 > getRepositoryUri(marketplace: IMarketplaceReference, marketplaceType?: MarketplaceType): URI;
58 >
59 > /**
60 > * Returns the local install URI for a plugin source directory inside its
61 > * marketplace repository cache.
62 > */
63 > getPluginInstallUri(plugin: IMarketplacePlugin): URI;
64 >
65 > /**
66 > * Ensures a marketplace repository is cloned locally and returns its cache URI.
67 > */
68 > ensureRepository(marketplace: IMarketplaceReference, options?: IEnsureRepositoryOptions): Promise<URI>;
69 >
70 > /**
71 > * Pulls latest changes for a cloned marketplace repository.
72 > * Returns `true` if the pull brought in new changes.
73 > */
74 > pullRepository(marketplace: IMarketplaceReference, options?: IPullRepositoryOptions): Promise<boolean>;
75 >
76 > /**
77 > * Returns the local install URI for a plugin based on its
78 > * {@link IPluginSourceDescriptor}. For non-relative-path sources
79 > * (github, url, npm, pip), this computes a cache location independent
80 > * of the marketplace repository.
81 > */
82 > getPluginSourceInstallUri(sourceDescriptor: IPluginSourceDescriptor): URI;
83 >
84 > /**
85 > * Ensures the plugin source is available locally. For github/url sources
86 > * this clones the repository into the cache. For npm/pip sources this is
87 > * a no-op (installation via terminal is handled by the install service).
88 > */
89 > ensurePluginSource(plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise<URI>;
90 >
91 > /**
92 > * Updates a plugin source that is stored outside the marketplace repository.
93 > * For github/url sources this pulls latest changes and reapplies pinned
94 > * ref/sha checkout. For npm/pip sources this is a no-op.
95 > * Returns `true` if the update brought in new changes.
96 > */
97 > updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise<boolean>;
98 >
99 > /**
100 > * Returns the {@link IPluginSource} strategy for the given
101 > * source kind, allowing callers to invoke kind-specific operations
102 > * (install, update, label, etc.) directly.
103 > */
104 > getPluginSource(kind: PluginSourceKind): IPluginSource;
105 >
106 > /**
107 > * Cleans up on-disk cache for a plugin source that owns its own install
108 > * directory. For marketplace-relative sources this is a no-op (they share
109 > * the marketplace repository cache). For direct sources (github, url, npm,
110 > * pip) the cache directory is deleted.
111 > *
112 > * When {@link otherInstalledDescriptors} is provided, deletion is skipped
113 > * if any of those descriptors share the same cleanup target directory
114 > * (e.g. multiple plugins installed from the same cloned repository).
115 > *
116 > * This is best-effort: failures are logged but do not throw.
117 > */
118 > cleanupPluginSource(plugin: IMarketplacePlugin, otherInstalledDescriptors?: readonly IPluginSourceDescriptor[]): Promise<void>;
119 >
120 > /**
121 > * Silently fetches remote refs for a cloned marketplace repository and
122 > * returns whether the local branch is behind the remote (i.e. updates
123 > * are available). Returns `false` if the repo is not cloned or on
124 > * network failure.
125 > */
126 > fetchRepository(marketplace: IMarketplaceReference): Promise<boolean>;
127 > }
src/vs/workbench/contrib/chat/common/plugins/fileBackedInstalledPluginsStore.ts 123 introduced LOC · 13 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- fileBackedInstalledPluginsStore.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { RunOnceScheduler, ThrottledDelayer } from '../../../../../base/common/async.js';
7 > import { VSBuffer } from '../../../../../base/common/buffer.js';
8 > import { Disposable } from '../../../../../base/common/lifecycle.js';
9 > import { revive } from '../../../../../base/common/marshalling.js';
10 > import { IObservable, ITransaction, observableValue } from '../../../../../base/common/observable.js';
11 > import { isEqual, joinPath } from '../../../../../base/common/resources.js';
12 > import { URI, UriComponents } from '../../../../../base/common/uri.js';
13 > import { IFileService } from '../../../../../platform/files/common/files.js';
14 > import { ILogService } from '../../../../../platform/log/common/log.js';
15 > import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';
16 >
17 > const INSTALLED_JSON_FILENAME = 'installed.json';
18 > const INSTALLED_JSON_VERSION = 1;
19 >
20 > /** Legacy storage key used before migration to file-backed store. */
21 > const LEGACY_INSTALLED_PLUGINS_STORAGE_KEY = 'chat.plugins.installed.v1';
22 > /** Legacy storage key for the marketplace index that cached old URI paths. */
23 > const LEGACY_MARKETPLACE_INDEX_STORAGE_KEY = 'chat.plugins.marketplaces.index.v1';
24 >
25 > /**
26 > * Minimal entry stored in `installed.json`. URIs are serialised as strings
27 > * so that external tools can read and write the file without depending on
28 > * VS Code internal URI representations. The optional `name` identifies the
29 > * marketplace plugin and lets VS Code re-read the full descriptor from the
30 > * marketplace when needed.
31 > */
32 > interface IInstalledJsonEntry {
33 > readonly pluginUri: string;
34 > readonly marketplace: string;
35 > readonly name?: string;
36 > }
37 >
38 > /**
39 > * On-disk schema for `installed.json`.
40 > */
41 > interface IInstalledJson {
42 > readonly version: number;
43 > readonly installed: readonly IInstalledJsonEntry[];
44 > }
45 >
46 > /**
47 > * In-memory representation of an installed plugin entry.
48 > */
49 > export interface IStoredInstalledPlugin {
50 > readonly pluginUri: URI;
51 > readonly marketplace: string;
52 > readonly name?: string;
53 > }
54 >
55 > /**
56 > * An observable store for installed agent plugins that is backed by a
57 > * `installed.json` file within the agent-plugins directory. This makes
58 > * the installed-plugin manifest discoverable by external tools (CLIs,
59 > * other editors, etc.) without depending on VS Code internals.
60 > *
61 > * The on-disk format stores only the plugin URI (as a string), marketplace
62 > * identifier, and plugin name. Full plugin metadata (description, source
63 > * descriptor, etc.) is read from marketplace data by the discovery layer -
64 > * keeping a single source of truth.
65 > *
66 > * On construction the store:
67 > * 1. Attempts to read `installed.json` from the agent-plugins directory.
68 > * 2. If no file exists, migrates data from the legacy {@link StorageService}
69 > * key (`chat.plugins.installed.v1`), rebasing plugin URIs from the old
70 > * cache directory to the new agent-plugins directory.
71 > * 3. Sets up a correlated file watcher so that external edits to
72 > * `installed.json` are picked up automatically.
73 > *
74 > * Write operations update the in-memory observable synchronously and
75 > * schedule a debounced file write so that rapid successive mutations
76 > * (e.g. batch enables) are coalesced into a single I/O operation.
77 > */
78 > export class FileBackedInstalledPluginsStore extends Disposable {
79 > private readonly _installed = observableValue<readonly IStoredInstalledPlugin[]>('file/installed.json', []);
80 > private readonly _fileUri: URI;
81 > private readonly _writeDelayer: ThrottledDelayer<void>;
82 > private _suppressFileWatch = false;
83 > private _initialized = false;
84 >
85 > readonly value: IObservable<readonly IStoredInstalledPlugin[]> = this._installed;
86 >
87 > constructor(
88 private readonly _agentPluginsHome: URI,
89 private readonly _oldCacheRoot: URI | undefined,
122 this._setupFileWatcher();
123 }
125 > // --- File I/O ----------------------------------------------------------------
126 >
127 > private async _readFromFile(): Promise<readonly IStoredInstalledPlugin[] | undefined> {
128 try {
129 const exists = await this._fileService.exists(this._fileUri);
151 }
152 }
154 > private _scheduleWrite(): void {
155 void this._writeDelayer.trigger(async () => {
156 await this._writeToFile();
157 });
158 }
160 > private async _writeToFile(): Promise<boolean> {
161 const entries: IInstalledJsonEntry[] = this.get().map(e => ({
162 pluginUri: e.pluginUri.toString(),
183 }
184 }
186 > // --- File watching ------------------------------------------------------------
187 >
188 > private _setupFileWatcher(): void {
189 if (typeof this._fileService.createWatcher !== 'function') {
190 return;
201 }));
202 }
204 > private async _onFileChanged(): Promise<void> {
205 const read = await this._readFromFile();
206 if (read !== undefined) {
214 }
215 }
217 > // --- Write-through to file ----------------------------------------------------
218 >
219 > private _setValue(newValue: readonly IStoredInstalledPlugin[], tx: ITransaction | undefined, scheduleWrite: boolean): void {
220 this._installed.set(newValue, tx);
221 // Only schedule writes after initialization and when not processing
225 }
226 }
228 > // --- Migration from legacy storage -------------------------------------------
229 >
230 > private async _migrateFromStorage(): Promise<void> {
231 const raw = this._storageService.get(LEGACY_INSTALLED_PLUGINS_STORAGE_KEY, StorageScope.APPLICATION);
232 if (!raw) {
266 }
267 }
269 > /**
270 > * If the plugin URI was under the old cache root, rebase it to the
271 > * new agent-plugins directory. Otherwise, return `undefined` to keep
272 > * the original.
273 > */
274 > private _rebasePluginUri(uri: URI): URI | undefined {
275 if (!this._oldCacheRoot) {
276 return undefined;