@max-null/dsh-plugin-center 0.1.0

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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Plugin-center engine: the process-local composition of metadata, market,
3
+ * and update detection. Read-only over the Loader except install/update, which
4
+ * delegate to pnpm (mirroring `dsh plugin add`). The market catalog is fetched
5
+ * in batches behind a process-local cache, so listMarket returns what is ready
6
+ * so far and the client waterfalls until done.
7
+ */
8
+ import { Service, type Context } from '@deepseek-ai/cordis';
9
+ import { type InstalledPlugin } from './meta.ts';
10
+ import { type MarketPlugin } from './market.ts';
11
+ import { type UpdateDigest } from './update.ts';
12
+ declare module '@deepseek-ai/cordis' {
13
+ interface Context {
14
+ /** The plugin-center engine (provided by this package's host half). */
15
+ pluginCenter: PluginCenterEngine;
16
+ }
17
+ }
18
+ /** Which market directory the client wants to browse. */
19
+ export type MarketSource = 'all' | 'awesome' | 'oh-my-dsh';
20
+ /** What's New read-mark result, returned by listMarket so the client waterfalls. */
21
+ export interface MarketSnapshot {
22
+ plugins: MarketPlugin[];
23
+ done: boolean;
24
+ }
25
+ export declare class PluginCenterEngine extends Service {
26
+ static inject: string[];
27
+ private awesomeCache;
28
+ private awesomeDone;
29
+ private awesomeFetching;
30
+ private ohMyDshCache;
31
+ private ohMyDshDone;
32
+ private ohMyDshFetching;
33
+ private installedNamesCache;
34
+ private updatesCache;
35
+ private readonly updatesTtlMs;
36
+ constructor(ctx: Context);
37
+ /** Background preload of market + installed metadata; failures fall back to lazy load. */
38
+ private warmup;
39
+ /** The profile directory (cordis.yml anchor) — the resolution and install cwd. */
40
+ private get baseUrl();
41
+ /** DSH home directory, for the read-mark persistence file. */
42
+ private get dshHome();
43
+ private get readVersionsPath();
44
+ /** Durable read-mark: which plugin version the user has already seen. */
45
+ readVersions(): Promise<Record<string, string>>;
46
+ /** Persist the read-mark (best-effort; a quota/IO failure just loses the mark). */
47
+ markRead(versions: Record<string, string>): Promise<void>;
48
+ /** Current DSH version, read from the installed @deepseek-ai/dsh package. */
49
+ dshVersion(): Promise<string>;
50
+ /** Non-group Loader entries, cross-matched with market categories. */
51
+ listInstalled(): Promise<InstalledPlugin[]>;
52
+ /** Start the awesome catalog fetch once, keeping the process-local cache. */
53
+ private prefetchAwesome;
54
+ /** Backfill npm latest versions into the awesome cache (best-effort, concurrent). */
55
+ private fillNpmVersions;
56
+ /** Single-registry latest version for the market bulk backfill (npmmirror is fast). */
57
+ private fastNpmVersion;
58
+ /** Start the Oh-My-DSH fetch once (single PLUGINS.md parse). */
59
+ private prefetchOhMyDsh;
60
+ /** Installed plugin names (no file IO) — cached so market polling stays cheap. */
61
+ private installedNames;
62
+ /** Market snapshot for one source: what is cached so far, plus whether done. */
63
+ listMarket(source?: MarketSource): Promise<MarketSnapshot>;
64
+ /** Detect updates for every installed third-party/local plugin, TTL-cached. */
65
+ checkUpdates(sinceIso: string): Promise<UpdateDigest[]>;
66
+ install(spec: string): Promise<boolean>;
67
+ update(name: string): Promise<boolean>;
68
+ /** Temporary diagnostics for the empty-update bug; removed once root-caused. */
69
+ debug(): Promise<{
70
+ baseUrl: string;
71
+ installed: {
72
+ name: string;
73
+ version: string | null;
74
+ source: string;
75
+ }[];
76
+ }>;
77
+ }
package/dist/engine.js ADDED
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Plugin-center engine: the process-local composition of metadata, market,
3
+ * and update detection. Read-only over the Loader except install/update, which
4
+ * delegate to pnpm (mirroring `dsh plugin add`). The market catalog is fetched
5
+ * in batches behind a process-local cache, so listMarket returns what is ready
6
+ * so far and the client waterfalls until done.
7
+ */
8
+ import { Service } from '@deepseek-ai/cordis';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { dirname, join } from 'node:path';
11
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
12
+ import { buildInstalledPlugin, resolvePackage } from "./meta.js";
13
+ import { fetchAwesomePluginsJson, fetchOhMyDshOverrides, fetchOhMyDshPlugins, mapConcurrent, mergePlugins } from "./market.js";
14
+ import { detectUpdate, installPlugin, updatePlugin } from "./update.js";
15
+ /** Runtime mirror of cordis FiberState (a cross-package const enum). */
16
+ const FIBER_PHASE = {
17
+ 0: 'pending',
18
+ 1: 'loading',
19
+ 2: 'active',
20
+ 3: 'failed',
21
+ 4: null,
22
+ 5: 'unloading',
23
+ };
24
+ /** Sources that participate in update detection (official/builtin follow DSH itself). */
25
+ const UPDATABLE = new Set(['installed', 'local']);
26
+ export class PluginCenterEngine extends Service {
27
+ static inject = ['loader'];
28
+ awesomeCache = [];
29
+ awesomeDone = false;
30
+ awesomeFetching = false;
31
+ ohMyDshCache = [];
32
+ ohMyDshDone = false;
33
+ ohMyDshFetching = false;
34
+ installedNamesCache = null;
35
+ updatesCache = null;
36
+ updatesTtlMs = 5 * 60_000;
37
+ constructor(ctx) {
38
+ super(ctx, 'pluginCenter');
39
+ // Warm caches in the background once the host is up, so opening the panel
40
+ // hits preloaded data instead of fetching on first paint. The loader is
41
+ // already settled (injected dependency), so entries() is complete here.
42
+ void this.warmup();
43
+ }
44
+ /** Background preload of market + installed metadata; failures fall back to lazy load. */
45
+ async warmup() {
46
+ try {
47
+ await this.listInstalled();
48
+ }
49
+ catch { /* listInstalled is re-run on demand */ }
50
+ this.prefetchAwesome();
51
+ this.prefetchOhMyDsh();
52
+ }
53
+ /** The profile directory (cordis.yml anchor) — the resolution and install cwd. */
54
+ get baseUrl() {
55
+ if (this.ctx.baseUrl === undefined) {
56
+ throw new Error('plugin-center: ctx.baseUrl is unset — the host needs the profile anchor');
57
+ }
58
+ const raw = this.ctx.baseUrl;
59
+ // ctx.baseUrl is a file:// URL; createRequire and pnpm need a plain path.
60
+ return raw.startsWith('file://') ? fileURLToPath(raw) : raw;
61
+ }
62
+ /** DSH home directory, for the read-mark persistence file. */
63
+ get dshHome() {
64
+ const env = process.env.DSH_HOME;
65
+ if (env !== undefined && env !== '')
66
+ return env;
67
+ return dirname(dirname(this.baseUrl)); // baseUrl = …/profiles/<name> → …/
68
+ }
69
+ get readVersionsPath() {
70
+ return join(this.dshHome, 'plugin-center-read-versions.json');
71
+ }
72
+ /** Durable read-mark: which plugin version the user has already seen. */
73
+ async readVersions() {
74
+ try {
75
+ return JSON.parse(await readFile(this.readVersionsPath, 'utf8'));
76
+ }
77
+ catch {
78
+ return {};
79
+ }
80
+ }
81
+ /** Persist the read-mark (best-effort; a quota/IO failure just loses the mark). */
82
+ async markRead(versions) {
83
+ try {
84
+ const path = this.readVersionsPath;
85
+ await mkdir(dirname(path), { recursive: true });
86
+ await writeFile(path, JSON.stringify(versions), 'utf8');
87
+ }
88
+ catch { /* best-effort */ }
89
+ }
90
+ /** Current DSH version, read from the installed @deepseek-ai/dsh package. */
91
+ async dshVersion() {
92
+ const resolved = await resolvePackage(this.baseUrl, '@deepseek-ai/dsh');
93
+ return resolved?.pkg.version ?? '0.0.0';
94
+ }
95
+ /** Non-group Loader entries, cross-matched with market categories. */
96
+ async listInstalled() {
97
+ const views = [];
98
+ for (const entry of this.ctx.loader.entries()) {
99
+ if (entry.options.group)
100
+ continue;
101
+ views.push({
102
+ id: entry.id,
103
+ name: entry.options.name,
104
+ disabled: entry.disabled,
105
+ fiberPhase: entry.fiber === undefined ? null : FIBER_PHASE[entry.fiber.state],
106
+ });
107
+ }
108
+ const categoryByName = new Map(mergePlugins([this.awesomeCache, this.ohMyDshCache]).map(m => [m.name, m.categories]));
109
+ const plugins = await Promise.all(views.map(v => buildInstalledPlugin(this.baseUrl, v)));
110
+ // Sort local dev first, then third-party installs, then official, then builtin.
111
+ const SOURCE_ORDER = { local: 0, installed: 1, official: 2, builtin: 3 };
112
+ return plugins
113
+ .map(p => ({ ...p, categories: categoryByName.get(p.name) ?? [] }))
114
+ .sort((a, b) => SOURCE_ORDER[a.source] - SOURCE_ORDER[b.source]);
115
+ }
116
+ /** Start the awesome catalog fetch once, keeping the process-local cache. */
117
+ prefetchAwesome() {
118
+ if (this.awesomeFetching || this.awesomeDone)
119
+ return;
120
+ this.awesomeFetching = true;
121
+ void (async () => {
122
+ try {
123
+ const [plugins, overrides] = await Promise.all([fetchAwesomePluginsJson(), fetchOhMyDshOverrides()]);
124
+ const merged = mergePlugins([plugins]).map((p) => {
125
+ const override = overrides[p.name];
126
+ return override?.category !== undefined && override.category !== ''
127
+ ? { ...p, categories: [...new Set([...p.categories, override.category])] }
128
+ : p;
129
+ });
130
+ this.awesomeCache = merged;
131
+ await this.fillNpmVersions();
132
+ }
133
+ catch { /* keep whatever cached so far */ }
134
+ this.awesomeDone = true;
135
+ this.awesomeFetching = false;
136
+ })();
137
+ }
138
+ /** Backfill npm latest versions into the awesome cache (best-effort, concurrent). */
139
+ async fillNpmVersions() {
140
+ const targets = this.awesomeCache.filter(p => p.npm !== null);
141
+ if (targets.length === 0)
142
+ return;
143
+ const versions = await mapConcurrent(targets.map(p => p.npm), 50, this.fastNpmVersion);
144
+ const versionByNpm = new Map();
145
+ targets.forEach((p, i) => {
146
+ const version = versions[i] ?? null;
147
+ if (version !== null)
148
+ versionByNpm.set(p.npm, version);
149
+ });
150
+ this.awesomeCache = this.awesomeCache.map(p => p.npm !== null && versionByNpm.has(p.npm) ? { ...p, version: versionByNpm.get(p.npm) } : p);
151
+ }
152
+ /** Single-registry latest version for the market bulk backfill (npmmirror is fast). */
153
+ async fastNpmVersion(name) {
154
+ try {
155
+ const res = await fetch(`https://registry.npmmirror.com/${name}/latest`, {
156
+ signal: AbortSignal.timeout(5000),
157
+ });
158
+ if (!res.ok)
159
+ return null;
160
+ return (await res.json()).version ?? null;
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
166
+ /** Start the Oh-My-DSH fetch once (single PLUGINS.md parse). */
167
+ prefetchOhMyDsh() {
168
+ if (this.ohMyDshFetching || this.ohMyDshDone)
169
+ return;
170
+ this.ohMyDshFetching = true;
171
+ void (async () => {
172
+ try {
173
+ this.ohMyDshCache = mergePlugins([await fetchOhMyDshPlugins()]);
174
+ }
175
+ catch { /* empty on failure */ }
176
+ this.ohMyDshDone = true;
177
+ this.ohMyDshFetching = false;
178
+ })();
179
+ }
180
+ /** Installed plugin names (no file IO) — cached so market polling stays cheap. */
181
+ async installedNames() {
182
+ if (this.installedNamesCache !== null)
183
+ return this.installedNamesCache;
184
+ const names = new Set();
185
+ for (const entry of this.ctx.loader.entries()) {
186
+ if (!entry.options.group)
187
+ names.add(entry.options.name);
188
+ }
189
+ this.installedNamesCache = names;
190
+ return names;
191
+ }
192
+ /** Market snapshot for one source: what is cached so far, plus whether done. */
193
+ async listMarket(source = 'all') {
194
+ const installedNames = await this.installedNames();
195
+ const decorate = (plugins) => plugins.map(p => ({ ...p, installed: installedNames.has(p.name) }));
196
+ if (source === 'awesome') {
197
+ this.prefetchAwesome();
198
+ return { plugins: decorate(this.awesomeCache), done: this.awesomeDone };
199
+ }
200
+ if (source === 'oh-my-dsh') {
201
+ this.prefetchOhMyDsh();
202
+ return { plugins: decorate(this.ohMyDshCache), done: this.ohMyDshDone };
203
+ }
204
+ this.prefetchAwesome();
205
+ this.prefetchOhMyDsh();
206
+ return {
207
+ plugins: decorate(mergePlugins([this.awesomeCache, this.ohMyDshCache])),
208
+ done: this.awesomeDone && this.ohMyDshDone,
209
+ };
210
+ }
211
+ /** Detect updates for every installed third-party/local plugin, TTL-cached. */
212
+ async checkUpdates(sinceIso) {
213
+ const now = Date.now();
214
+ const hit = this.updatesCache;
215
+ if (hit !== null && hit.since === sinceIso && now - hit.at < this.updatesTtlMs)
216
+ return hit.digests;
217
+ const [installed, localDsh] = await Promise.all([this.listInstalled(), this.dshVersion()]);
218
+ const candidates = installed.filter(p => UPDATABLE.has(p.source) && p.version !== null);
219
+ const digests = await Promise.all(candidates.map(p => detectUpdate(p.name, p.version, p.repoUrl, p.compatRange, localDsh, sinceIso)));
220
+ this.updatesCache = { since: sinceIso, at: now, digests: digests.filter((d) => d !== null) };
221
+ return this.updatesCache.digests;
222
+ }
223
+ async install(spec) {
224
+ const ok = await installPlugin(spec, this.baseUrl);
225
+ this.installedNamesCache = null;
226
+ this.updatesCache = null;
227
+ return ok;
228
+ }
229
+ async update(name) {
230
+ const ok = await updatePlugin(name, this.baseUrl);
231
+ this.installedNamesCache = null;
232
+ this.updatesCache = null;
233
+ return ok;
234
+ }
235
+ /** Temporary diagnostics for the empty-update bug; removed once root-caused. */
236
+ async debug() {
237
+ return {
238
+ baseUrl: this.baseUrl,
239
+ installed: (await this.listInstalled()).map(p => ({ name: p.name, version: p.version, source: p.source })),
240
+ };
241
+ }
242
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `dsh-plugin-center` host half: mounts the engine and its loopback RPC.
3
+ * The browser half (`./client`) is picked up through the package's `dsh.client`
4
+ * declaration; this half registers the process-local engine + RPC channel.
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ export { PluginCenterEngine } from './engine.ts';
8
+ export type { InstalledPlugin, PluginSource } from './meta.ts';
9
+ export type { MarketPlugin } from './market.ts';
10
+ export type { UpdateDigest } from './update.ts';
11
+ export { compareVersions, parseVersion, satisfies } from './semver.ts';
12
+ export declare const name = "dsh-plugin-center";
13
+ /** The engine registers its own `loader` dependency; the gateway follows it. */
14
+ export declare const inject: string[];
15
+ export declare function apply(ctx: Context): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { PluginCenterEngine } from "./engine.js";
2
+ import { PluginCenterRpc } from "./rpc.js";
3
+ export { PluginCenterEngine } from "./engine.js";
4
+ export { compareVersions, parseVersion, satisfies } from "./semver.js";
5
+ export const name = 'dsh-plugin-center';
6
+ /** The engine registers its own `loader` dependency; the gateway follows it. */
7
+ export const inject = ['loader'];
8
+ export async function apply(ctx) {
9
+ await ctx.plugin(PluginCenterEngine);
10
+ await ctx.plugin(PluginCenterRpc);
11
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Community market: real-time aggregation of multiple plugin directories.
3
+ * Fetch is host-side (no browser CSP); merge dedupes by repo name and unions
4
+ * categories. Sources degrade independently — one failing source never blocks
5
+ * the rest.
6
+ */
7
+ /** One plugin as the market surface exposes it. */
8
+ export interface MarketPlugin {
9
+ name: string;
10
+ url: string;
11
+ /** Install spec (`github:owner/repo` or an npm package name), from the source. */
12
+ spec: string;
13
+ categories: string[];
14
+ description: {
15
+ en: string;
16
+ zh: string;
17
+ };
18
+ stars: number | null;
19
+ /** npm package name when the plugin is published there, else null. */
20
+ npm: string | null;
21
+ /** Latest published version (npm), null until fetched / for non-npm plugins. */
22
+ version: string | null;
23
+ installed: boolean;
24
+ }
25
+ /** A per-source plugin record before merging. */
26
+ interface RawPlugin {
27
+ name: string;
28
+ url: string;
29
+ spec: string;
30
+ categories: string[];
31
+ description: {
32
+ en: string;
33
+ zh: string;
34
+ };
35
+ stars: number | null;
36
+ npm: string | null;
37
+ }
38
+ /** Map a bounded set of fetches concurrently, keeping per-fetch failures as null. */
39
+ export declare function mapConcurrent<T>(items: readonly string[], limit: number, fn: (item: string) => Promise<T>): Promise<(T | null)[]>;
40
+ /**
41
+ * Fetch the built awesome catalog in one request — it is pre-enriched with
42
+ * GitHub stars and npm package names, so no per-plugin API calls are needed.
43
+ */
44
+ export declare function fetchAwesomePluginsJson(): Promise<RawPlugin[]>;
45
+ /** Fetch Oh-My-DSH's curated overrides (min_stars filter + category/note overrides). */
46
+ export declare function fetchOhMyDshOverrides(): Promise<Record<string, {
47
+ category?: string;
48
+ note?: string;
49
+ }>>;
50
+ /** Parse Oh-My-DSH's PLUGINS.md (markdown table, sectioned by category). */
51
+ export declare function fetchOhMyDshPlugins(): Promise<RawPlugin[]>;
52
+ /** Merge raw per-source records by repo name: union categories, keep non-empty desc/stars. */
53
+ export declare function mergePlugins(sources: RawPlugin[][]): MarketPlugin[];
54
+ export {};
package/dist/market.js ADDED
@@ -0,0 +1,125 @@
1
+ const UA = { 'User-Agent': 'dsh-plugin-center' };
2
+ async function rawText(url) {
3
+ const res = await fetch(url, { signal: AbortSignal.timeout(20000) });
4
+ if (!res.ok)
5
+ throw new Error(`HTTP ${res.status}: ${url}`);
6
+ return res.text();
7
+ }
8
+ /** Map a bounded set of fetches concurrently, keeping per-fetch failures as null. */
9
+ export async function mapConcurrent(items, limit, fn) {
10
+ const results = new Array(items.length).fill(null);
11
+ let next = 0;
12
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
13
+ while (next < items.length) {
14
+ const i = next++;
15
+ try {
16
+ results[i] = await fn(items[i]);
17
+ }
18
+ catch { /* degrade to null */ }
19
+ }
20
+ });
21
+ await Promise.all(workers);
22
+ return results;
23
+ }
24
+ /**
25
+ * Fetch the built awesome catalog in one request — it is pre-enriched with
26
+ * GitHub stars and npm package names, so no per-plugin API calls are needed.
27
+ */
28
+ export async function fetchAwesomePluginsJson() {
29
+ const res = await fetch('https://awesome-dsh-plugin.com/plugins.json', {
30
+ headers: UA,
31
+ signal: AbortSignal.timeout(30000),
32
+ });
33
+ if (!res.ok)
34
+ throw new Error(`awesome plugins.json: HTTP ${res.status}`);
35
+ const json = await res.json();
36
+ return json.plugins.map(p => ({
37
+ name: `${p.owner}/${p.name}`,
38
+ url: p.url,
39
+ spec: p.install.replace(/^.*\badd\s+/, ''),
40
+ categories: typeof p.category === 'string' && p.category !== '' ? [p.category] : [],
41
+ description: { en: p.description?.en ?? '', zh: p.description?.zh ?? '' },
42
+ stars: typeof p.stars === 'number' ? p.stars : null,
43
+ npm: typeof p.npm === 'string' && p.npm !== '' ? p.npm : null,
44
+ }));
45
+ }
46
+ /** Fetch Oh-My-DSH's curated overrides (min_stars filter + category/note overrides). */
47
+ export async function fetchOhMyDshOverrides() {
48
+ try {
49
+ const res = await fetch('https://raw.githubusercontent.com/like-study1/Oh-My-DSH/main/data/curated.json', {
50
+ signal: AbortSignal.timeout(20000),
51
+ });
52
+ if (!res.ok)
53
+ return {};
54
+ const json = await res.json();
55
+ return json.overrides ?? {};
56
+ }
57
+ catch {
58
+ return {};
59
+ }
60
+ }
61
+ /** Parse Oh-My-DSH's PLUGINS.md (markdown table, sectioned by category). */
62
+ export async function fetchOhMyDshPlugins() {
63
+ try {
64
+ const text = await rawText('https://raw.githubusercontent.com/like-study1/Oh-My-DSH/main/PLUGINS.md');
65
+ const plugins = [];
66
+ let category = '';
67
+ for (const line of text.split('\n')) {
68
+ const section = /^##\s+(.+?)(?:(\d+))?\s*$/.exec(line);
69
+ if (section !== null) {
70
+ category = section[1].trim();
71
+ continue;
72
+ }
73
+ const cells = line.split('|').map(c => c.trim());
74
+ if (cells.length < 7)
75
+ continue;
76
+ const link = /\[([^\]]+)\]\(([^)]+)\)/.exec(cells[1]);
77
+ if (link === null)
78
+ continue;
79
+ const stars = Number(cells[5]);
80
+ plugins.push({
81
+ name: link[1],
82
+ url: link[2],
83
+ spec: `github:${link[1]}`,
84
+ categories: category !== '' ? [category] : [],
85
+ description: { en: '', zh: cells[6] },
86
+ stars: Number.isFinite(stars) ? stars : null,
87
+ npm: null,
88
+ });
89
+ }
90
+ return plugins;
91
+ }
92
+ catch {
93
+ return [];
94
+ }
95
+ }
96
+ /** Merge raw per-source records by repo name: union categories, keep non-empty desc/stars. */
97
+ export function mergePlugins(sources) {
98
+ const map = new Map();
99
+ for (const items of sources) {
100
+ for (const item of items) {
101
+ const cur = map.get(item.name) ?? {
102
+ name: item.name,
103
+ url: item.url,
104
+ spec: item.spec,
105
+ categories: [],
106
+ description: { en: '', zh: '' },
107
+ stars: null,
108
+ npm: null,
109
+ version: null,
110
+ installed: false,
111
+ };
112
+ cur.categories = [...new Set([...cur.categories, ...item.categories])];
113
+ if (item.description.en !== '')
114
+ cur.description.en = item.description.en;
115
+ if (item.description.zh !== '')
116
+ cur.description.zh = item.description.zh;
117
+ if (item.stars !== null)
118
+ cur.stars = item.stars;
119
+ if (item.npm !== null)
120
+ cur.npm = item.npm;
121
+ map.set(item.name, cur);
122
+ }
123
+ }
124
+ return [...map.values()];
125
+ }
package/dist/meta.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /** Where an installed plugin came from. */
2
+ export type PluginSource = 'official' | 'installed' | 'local' | 'builtin';
3
+ /** One resolved installed plugin, ready for the Remote surface. */
4
+ export interface InstalledPlugin {
5
+ entryId: string;
6
+ name: string;
7
+ displayName: string;
8
+ version: string | null;
9
+ description: string | null;
10
+ source: PluginSource;
11
+ enabled: boolean;
12
+ fiberPhase: string | null;
13
+ compatRange: string | null;
14
+ repoUrl: string | null;
15
+ /** Community categories, cross-matched from the market catalog (empty until fetched). */
16
+ categories: string[];
17
+ }
18
+ /** Minimal package.json view this plugin reads. */
19
+ interface PackageJson {
20
+ name?: string;
21
+ version?: string;
22
+ description?: string;
23
+ repository?: string | {
24
+ url?: string;
25
+ };
26
+ peerDependencies?: Record<string, string>;
27
+ }
28
+ /** Compact a module specifier into a display name without guessing Loader id shape. */
29
+ export declare function displayName(specifier: string): string;
30
+ /**
31
+ * Resolve one Loader entry to its package.json. `file://` specs walk upward to
32
+ * the nearest directory holding a package.json; `cordis:*` builtins have none.
33
+ * Results are cached per (baseUrl, specifier) — the resolution is a pure read
34
+ * and never changes within a process, so the file I/O happens only once.
35
+ * @param baseUrl - profile directory (the cordis.yml anchor, `ctx.baseUrl`).
36
+ * @param specifier - the Loader entry's module specifier.
37
+ * @returns the parsed package.json and its directory, or null when unresolvable.
38
+ */
39
+ export declare function resolvePackage(baseUrl: string, specifier: string): Promise<{
40
+ pkg: PackageJson;
41
+ dir: string;
42
+ } | null>;
43
+ /** One Loader entry, the subset this plugin reads. */
44
+ export interface LoaderEntryView {
45
+ id: string;
46
+ name: string;
47
+ disabled: boolean;
48
+ group?: boolean;
49
+ fiberPhase: string | null;
50
+ }
51
+ /** Build the Remote-ready metadata for one Loader entry. */
52
+ export declare function buildInstalledPlugin(baseUrl: string, entry: LoaderEntryView): Promise<InstalledPlugin>;
53
+ export {};