@oh-my-pi/pi-coding-agent 16.0.0 → 16.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -133
- package/dist/cli.js +250 -218
- package/dist/types/config/model-resolver.d.ts +14 -0
- package/dist/types/config/settings-schema.d.ts +22 -0
- package/dist/types/discovery/helpers.d.ts +7 -0
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
- package/dist/types/exec/non-interactive-env.d.ts +2 -0
- package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
- package/dist/types/modes/types.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-manager.d.ts +4 -1
- package/dist/types/task/index.d.ts +21 -0
- package/dist/types/tools/github-cache.d.ts +5 -4
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/utils/markit.d.ts +8 -0
- package/dist/types/web/search/index.d.ts +2 -2
- package/dist/types/web/search/provider.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +44 -0
- package/src/cli/args.ts +2 -0
- package/src/collab/host.ts +1 -1
- package/src/config/model-resolver.ts +35 -1
- package/src/config/settings-schema.ts +23 -1
- package/src/discovery/claude-plugins.ts +3 -42
- package/src/discovery/github.ts +189 -6
- package/src/discovery/helpers.ts +11 -0
- package/src/eval/__tests__/prelude-agent.test.ts +73 -0
- package/src/eval/js/shared/prelude.txt +12 -3
- package/src/eval/py/prelude.py +26 -2
- package/src/exec/bash-executor.ts +2 -2
- package/src/exec/non-interactive-env.ts +71 -0
- package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
- package/src/extensibility/extensions/runner.ts +17 -1
- package/src/extensibility/plugins/loader.ts +157 -23
- package/src/extensibility/plugins/manager.ts +44 -36
- package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
- package/src/extensibility/plugins/runtime-config.ts +9 -0
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/internal-urls/issue-pr-protocol.ts +8 -4
- package/src/main.ts +5 -1
- package/src/modes/acp/acp-agent.ts +3 -3
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +7 -0
- package/src/modes/interactive-mode.ts +47 -0
- package/src/modes/rpc/rpc-mode.ts +3 -3
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/types.ts +5 -0
- package/src/prompts/agents/designer.md +8 -0
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +4 -1
- package/src/prompts/tools/eval.md +13 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/sdk.ts +9 -1
- package/src/session/agent-session.ts +260 -50
- package/src/session/messages.ts +1 -1
- package/src/session/session-manager.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +5 -2
- package/src/system-prompt.ts +7 -1
- package/src/task/executor.ts +105 -8
- package/src/task/index.ts +70 -9
- package/src/tools/github-cache.ts +32 -7
- package/src/tools/job.ts +14 -1
- package/src/utils/lang-from-path.ts +5 -0
- package/src/utils/markit.ts +24 -1
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +14 -2
|
@@ -9,6 +9,7 @@ import * as path from "node:path";
|
|
|
9
9
|
import { getPluginsLockfile, getPluginsNodeModules, getPluginsPackageJson, isEnoent } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { getConfigDirPaths } from "../../config";
|
|
11
11
|
import { installLegacyPiSpecifierShim } from "./legacy-pi-compat";
|
|
12
|
+
import { normalizePluginRuntimeConfig } from "./runtime-config";
|
|
12
13
|
import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPluginOverrides } from "./types";
|
|
13
14
|
|
|
14
15
|
installLegacyPiSpecifierShim();
|
|
@@ -28,9 +29,9 @@ installLegacyPiSpecifierShim();
|
|
|
28
29
|
async function loadRuntimeConfig(home?: string): Promise<PluginRuntimeConfig> {
|
|
29
30
|
const lockPath = getPluginsLockfile(home);
|
|
30
31
|
try {
|
|
31
|
-
return await Bun.file(lockPath).json();
|
|
32
|
+
return normalizePluginRuntimeConfig(await Bun.file(lockPath).json());
|
|
32
33
|
} catch (err) {
|
|
33
|
-
if (isEnoent(err)) return {
|
|
34
|
+
if (isEnoent(err)) return normalizePluginRuntimeConfig({});
|
|
34
35
|
throw err;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -142,29 +143,161 @@ export async function getEnabledPlugins(cwd: string, opts: { home?: string } = {
|
|
|
142
143
|
// Path Resolution
|
|
143
144
|
// =============================================================================
|
|
144
145
|
|
|
145
|
-
const
|
|
146
|
+
const MANIFEST_ENTRY_MODULE_EXTENSIONS = [".ts", ".js", ".mjs", ".cjs"];
|
|
147
|
+
const MANIFEST_ENTRY_INDEX_NAMES = MANIFEST_ENTRY_MODULE_EXTENSIONS.map(ext => `index${ext}`);
|
|
148
|
+
|
|
149
|
+
/** `.d.ts` / `.d.mts` / `.d.cts` TypeScript declaration files — never loadable as modules. */
|
|
150
|
+
const DECLARATION_FILE_RE = /\.d\.[mc]?ts$/;
|
|
151
|
+
|
|
152
|
+
/** A loadable module file: a .ts/.js/.mjs/.cjs that is not a declaration file. */
|
|
153
|
+
function isModuleFile(name: string): boolean {
|
|
154
|
+
return MANIFEST_ENTRY_MODULE_EXTENSIONS.includes(path.extname(name)) && !DECLARATION_FILE_RE.test(name);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** First `index.{ts,js,mjs,cjs}` inside `dir`, or null when none exists. */
|
|
158
|
+
function findDirectoryIndex(dir: string): string | null {
|
|
159
|
+
for (const name of MANIFEST_ENTRY_INDEX_NAMES) {
|
|
160
|
+
const candidate = path.join(dir, name);
|
|
161
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface DeclaredManifestEntries {
|
|
167
|
+
/** True when the directory's package.json declares a non-empty `omp`/`pi` `extensions` array. */
|
|
168
|
+
declared: boolean;
|
|
169
|
+
/** Resolved, existing module files for the declared entries (may be empty when declared files are missing). */
|
|
170
|
+
files: string[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Read the extension entries declared by `dir`'s own package.json `omp`/`pi`
|
|
175
|
+
* manifest. `declared` distinguishes "a manifest explicitly lists extensions"
|
|
176
|
+
* (authoritative — callers must not fall back to index/scan, so a missing
|
|
177
|
+
* declared file surfaces as a missing entry instead of silently loading a stale
|
|
178
|
+
* index) from "no manifest / no extensions field" (callers fall back to
|
|
179
|
+
* convention). Mirrors the manifest branch of the configured-directory (`-e`)
|
|
180
|
+
* scanner: a declared entry that is a file resolves to itself; one that is a
|
|
181
|
+
* directory resolves to its direct index.{ts,js,mjs,cjs}.
|
|
182
|
+
*/
|
|
183
|
+
function readDeclaredManifestEntries(dir: string): DeclaredManifestEntries {
|
|
184
|
+
let raw: string;
|
|
185
|
+
try {
|
|
186
|
+
raw = fs.readFileSync(path.join(dir, "package.json"), "utf8");
|
|
187
|
+
} catch {
|
|
188
|
+
return { declared: false, files: [] };
|
|
189
|
+
}
|
|
190
|
+
let pkg: { omp?: { extensions?: unknown }; pi?: { extensions?: unknown } };
|
|
191
|
+
try {
|
|
192
|
+
pkg = JSON.parse(raw) as { omp?: { extensions?: unknown }; pi?: { extensions?: unknown } };
|
|
193
|
+
} catch {
|
|
194
|
+
return { declared: false, files: [] };
|
|
195
|
+
}
|
|
196
|
+
const declared = (pkg.omp ?? pkg.pi)?.extensions;
|
|
197
|
+
if (!Array.isArray(declared) || declared.length === 0) {
|
|
198
|
+
return { declared: false, files: [] };
|
|
199
|
+
}
|
|
200
|
+
const files: string[] = [];
|
|
201
|
+
for (const entry of declared) {
|
|
202
|
+
if (typeof entry !== "string") continue;
|
|
203
|
+
const candidate = path.resolve(dir, entry);
|
|
204
|
+
let candidateStats: fs.Stats;
|
|
205
|
+
try {
|
|
206
|
+
candidateStats = fs.statSync(candidate);
|
|
207
|
+
} catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (candidateStats.isDirectory()) {
|
|
211
|
+
const index = findDirectoryIndex(candidate);
|
|
212
|
+
if (index) files.push(index);
|
|
213
|
+
} else {
|
|
214
|
+
files.push(candidate);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { declared: true, files };
|
|
218
|
+
}
|
|
146
219
|
|
|
147
220
|
/**
|
|
148
|
-
* Resolve a
|
|
149
|
-
*
|
|
150
|
-
* the
|
|
151
|
-
*
|
|
221
|
+
* Resolve a directory to its loadable extension module files, mirroring the
|
|
222
|
+
* configured-directory (`-e`) scanner in extensions/loader.ts:
|
|
223
|
+
* 1. the directory's own package.json `omp`/`pi` `extensions` entries —
|
|
224
|
+
* authoritative: a manifest that lists extensions suppresses the index/scan
|
|
225
|
+
* fallback, so a missing declared file is reported rather than silently
|
|
226
|
+
* replaced by a decoy index
|
|
227
|
+
* 2. a direct index.{ts,js,mjs,cjs}
|
|
228
|
+
* 3. one level of children: each direct *.{ts,js,mjs,cjs} file plus each
|
|
229
|
+
* sub-directory resolved by the same precedence (manifest, then index)
|
|
152
230
|
*/
|
|
153
|
-
function
|
|
231
|
+
function resolveDirectoryEntries(dir: string): string[] {
|
|
232
|
+
const manifest = readDeclaredManifestEntries(dir);
|
|
233
|
+
if (manifest.declared) return manifest.files;
|
|
234
|
+
|
|
235
|
+
const directIndex = findDirectoryIndex(dir);
|
|
236
|
+
if (directIndex) return [directIndex];
|
|
237
|
+
|
|
238
|
+
let children: string[];
|
|
239
|
+
try {
|
|
240
|
+
children = fs.readdirSync(dir);
|
|
241
|
+
} catch {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
const resolved: string[] = [];
|
|
245
|
+
for (const child of children.sort()) {
|
|
246
|
+
const childPath = path.join(dir, child);
|
|
247
|
+
let childStats: fs.Stats;
|
|
248
|
+
try {
|
|
249
|
+
// statSync follows symlinks, matching the configured-dir loader.
|
|
250
|
+
childStats = fs.statSync(childPath);
|
|
251
|
+
} catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (childStats.isDirectory()) {
|
|
255
|
+
const childManifest = readDeclaredManifestEntries(childPath);
|
|
256
|
+
if (childManifest.declared) {
|
|
257
|
+
resolved.push(...childManifest.files);
|
|
258
|
+
} else {
|
|
259
|
+
const index = findDirectoryIndex(childPath);
|
|
260
|
+
if (index) resolved.push(index);
|
|
261
|
+
}
|
|
262
|
+
} else if (isModuleFile(child)) {
|
|
263
|
+
resolved.push(childPath);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return resolved;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolve a plugin manifest entry to the loadable module files it names:
|
|
271
|
+
* - a file entry → that file
|
|
272
|
+
* - a directory:
|
|
273
|
+
* - when `expandDirectory` (the `extensions` key), resolved by
|
|
274
|
+
* {@link resolveDirectoryEntries} — its own package.json `omp`/`pi`
|
|
275
|
+
* `extensions`, then a direct index, then a one-level scan of
|
|
276
|
+
* sub-extensions — matching the pi `extensions/<name>/index.ts` convention
|
|
277
|
+
* and OMP's configured-directory (`-e`) extension loader
|
|
278
|
+
* - otherwise (tools/hooks/commands) only a direct index.{ts,js,mjs,cjs}.
|
|
279
|
+
* The sub-extension scan and the `omp`/`pi` `extensions` manifest are
|
|
280
|
+
* extensions-specific and must not hijack a non-extension directory entry
|
|
281
|
+
* (e.g. a `tools: "."` entry must still resolve `./index.ts`).
|
|
282
|
+
*
|
|
283
|
+
* Returns an empty array when nothing loadable exists at `joined`, letting
|
|
284
|
+
* callers flag a missing entry instead of silently dropping it.
|
|
285
|
+
*/
|
|
286
|
+
function resolveManifestEntryFiles(joined: string, expandDirectory: boolean): string[] {
|
|
154
287
|
let stats: fs.Stats;
|
|
155
288
|
try {
|
|
156
289
|
stats = fs.statSync(joined);
|
|
157
290
|
} catch {
|
|
158
|
-
return
|
|
291
|
+
return [];
|
|
159
292
|
}
|
|
160
|
-
if (stats.isDirectory()) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return null;
|
|
293
|
+
if (!stats.isDirectory()) {
|
|
294
|
+
return [joined];
|
|
295
|
+
}
|
|
296
|
+
if (expandDirectory) {
|
|
297
|
+
return resolveDirectoryEntries(joined);
|
|
166
298
|
}
|
|
167
|
-
|
|
299
|
+
const index = findDirectoryIndex(joined);
|
|
300
|
+
return index ? [index] : [];
|
|
168
301
|
}
|
|
169
302
|
|
|
170
303
|
/**
|
|
@@ -195,16 +328,17 @@ export function resolvePluginManifestEntries(
|
|
|
195
328
|
const declared: Array<{ entry: string; resolvedPath: string | null }> = [];
|
|
196
329
|
const manifest = plugin.manifest;
|
|
197
330
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
331
|
+
const expandDirectory = key === "extensions";
|
|
332
|
+
const resolveEntry = (entry: string): Array<{ entry: string; resolvedPath: string | null }> => {
|
|
333
|
+
const files = resolveManifestEntryFiles(path.join(plugin.path, entry), expandDirectory);
|
|
334
|
+
return files.length > 0 ? files.map(resolvedPath => ({ entry, resolvedPath })) : [{ entry, resolvedPath: null }];
|
|
335
|
+
};
|
|
202
336
|
|
|
203
337
|
const base = manifest[key];
|
|
204
338
|
if (base) {
|
|
205
339
|
const entries = Array.isArray(base) ? base : [base];
|
|
206
340
|
for (const entry of entries) {
|
|
207
|
-
declared.push(resolveEntry(entry));
|
|
341
|
+
declared.push(...resolveEntry(entry));
|
|
208
342
|
}
|
|
209
343
|
}
|
|
210
344
|
|
|
@@ -214,7 +348,7 @@ export function resolvePluginManifestEntries(
|
|
|
214
348
|
if (!enabledSet.has(featName)) continue;
|
|
215
349
|
if (feat[key]) {
|
|
216
350
|
for (const entry of feat[key]) {
|
|
217
|
-
declared.push(resolveEntry(entry));
|
|
351
|
+
declared.push(...resolveEntry(entry));
|
|
218
352
|
}
|
|
219
353
|
}
|
|
220
354
|
}
|
|
@@ -224,7 +358,7 @@ export function resolvePluginManifestEntries(
|
|
|
224
358
|
if (!feat.default) continue;
|
|
225
359
|
if (feat[key]) {
|
|
226
360
|
for (const entry of feat[key]) {
|
|
227
|
-
declared.push(resolveEntry(entry));
|
|
361
|
+
declared.push(...resolveEntry(entry));
|
|
228
362
|
}
|
|
229
363
|
}
|
|
230
364
|
}
|
|
@@ -15,6 +15,7 @@ import { type GitSource, parseGitUrl } from "./git-url";
|
|
|
15
15
|
import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "./legacy-pi-compat";
|
|
16
16
|
import { resolvePluginManifestEntries } from "./loader";
|
|
17
17
|
import { extractPackageName, parsePluginSpec } from "./parser";
|
|
18
|
+
import { normalizePluginRuntimeConfig } from "./runtime-config";
|
|
18
19
|
import type {
|
|
19
20
|
DoctorCheck,
|
|
20
21
|
DoctorOptions,
|
|
@@ -124,11 +125,11 @@ export class PluginManager {
|
|
|
124
125
|
async #loadRuntimeConfig(): Promise<PluginRuntimeConfig> {
|
|
125
126
|
const lockPath = getPluginsLockfile();
|
|
126
127
|
try {
|
|
127
|
-
return await Bun.file(lockPath).json();
|
|
128
|
+
return normalizePluginRuntimeConfig(await Bun.file(lockPath).json());
|
|
128
129
|
} catch (err) {
|
|
129
|
-
if (isEnoent(err)) return {
|
|
130
|
+
if (isEnoent(err)) return normalizePluginRuntimeConfig({});
|
|
130
131
|
logger.warn("Failed to load plugin runtime config", { path: lockPath, error: String(err) });
|
|
131
|
-
return {
|
|
132
|
+
return normalizePluginRuntimeConfig({});
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
@@ -204,6 +205,17 @@ export class PluginManager {
|
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
|
|
208
|
+
#collectInstalledNames(deps: Record<string, string>, config: PluginRuntimeConfig): Set<string> {
|
|
209
|
+
const installedNames = new Set<string>();
|
|
210
|
+
for (const name of Object.keys(deps)) {
|
|
211
|
+
installedNames.add(name);
|
|
212
|
+
}
|
|
213
|
+
for (const name of Object.keys(config.plugins)) {
|
|
214
|
+
installedNames.add(name);
|
|
215
|
+
}
|
|
216
|
+
return installedNames;
|
|
217
|
+
}
|
|
218
|
+
|
|
207
219
|
async #snapshotInstalledPackage(actualName: string | undefined): Promise<PluginPackageSnapshot | null> {
|
|
208
220
|
if (!actualName) {
|
|
209
221
|
return null;
|
|
@@ -489,21 +501,22 @@ export class PluginManager {
|
|
|
489
501
|
*/
|
|
490
502
|
async list(): Promise<InstalledPlugin[]> {
|
|
491
503
|
const pkgJsonPath = getPluginsPackageJson();
|
|
492
|
-
let
|
|
504
|
+
let deps: Record<string, string> = {};
|
|
493
505
|
try {
|
|
494
|
-
pkg = await Bun.file(pkgJsonPath).json();
|
|
506
|
+
const pkg: { dependencies?: Record<string, string> } = await Bun.file(pkgJsonPath).json();
|
|
507
|
+
deps = pkg.dependencies ?? {};
|
|
495
508
|
} catch (err) {
|
|
496
|
-
if (isEnoent(err))
|
|
497
|
-
throw err;
|
|
509
|
+
if (!isEnoent(err)) throw err;
|
|
498
510
|
}
|
|
499
511
|
|
|
500
|
-
const deps = pkg.dependencies || {};
|
|
501
512
|
const projectOverrides = await this.#loadProjectOverrides();
|
|
502
513
|
const config = await this.#ensureConfigLoaded();
|
|
503
514
|
const plugins: InstalledPlugin[] = [];
|
|
515
|
+
const installedNames = this.#collectInstalledNames(deps, config);
|
|
504
516
|
|
|
505
|
-
for (const
|
|
506
|
-
const
|
|
517
|
+
for (const name of installedNames) {
|
|
518
|
+
const pluginPath = path.join(getPluginsNodeModules(), name);
|
|
519
|
+
const pluginPkgPath = path.join(pluginPath, "package.json");
|
|
507
520
|
let pluginPkg: { version: string; omp?: PluginManifest; pi?: PluginManifest };
|
|
508
521
|
try {
|
|
509
522
|
pluginPkg = await Bun.file(pluginPkgPath).json();
|
|
@@ -520,14 +533,13 @@ export class PluginManager {
|
|
|
520
533
|
enabled: true,
|
|
521
534
|
};
|
|
522
535
|
|
|
523
|
-
// Apply project overrides
|
|
524
536
|
const isDisabledInProject = projectOverrides.disabled?.includes(name) ?? false;
|
|
525
537
|
const projectFeatures = projectOverrides.features?.[name];
|
|
526
538
|
|
|
527
539
|
plugins.push({
|
|
528
540
|
name,
|
|
529
541
|
version: pluginPkg.version,
|
|
530
|
-
path:
|
|
542
|
+
path: pluginPath,
|
|
531
543
|
manifest,
|
|
532
544
|
enabledFeatures: projectFeatures ?? runtimeState.enabledFeatures,
|
|
533
545
|
enabled: runtimeState.enabled && !isDisabledInProject,
|
|
@@ -743,15 +755,14 @@ export class PluginManager {
|
|
|
743
755
|
message: hasNodeModules ? "Found" : "Missing (run npm install in plugins dir)",
|
|
744
756
|
});
|
|
745
757
|
|
|
746
|
-
if (!hasPkgJson) {
|
|
747
|
-
return checks;
|
|
748
|
-
}
|
|
749
758
|
const deps = pkg.dependencies || {};
|
|
750
759
|
const config = await this.#ensureConfigLoaded();
|
|
760
|
+
const installedNames = this.#collectInstalledNames(deps, config);
|
|
751
761
|
|
|
752
|
-
for (const
|
|
762
|
+
for (const name of installedNames) {
|
|
753
763
|
const pluginPath = path.join(nodeModulesPath, name);
|
|
754
764
|
const pluginPkgPath = path.join(pluginPath, "package.json");
|
|
765
|
+
const fromDependencies = name in deps;
|
|
755
766
|
|
|
756
767
|
let pluginPkg: { version: string; description?: string; omp?: PluginManifest; pi?: PluginManifest };
|
|
757
768
|
try {
|
|
@@ -759,13 +770,23 @@ export class PluginManager {
|
|
|
759
770
|
} catch (err) {
|
|
760
771
|
if (isEnoent(err)) {
|
|
761
772
|
if (!fs.existsSync(pluginPath)) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
773
|
+
if (fromDependencies) {
|
|
774
|
+
const fixed = options.fix ? await this.#fixMissingPlugin() : false;
|
|
775
|
+
checks.push({
|
|
776
|
+
name: `plugin:${name}`,
|
|
777
|
+
status: "error",
|
|
778
|
+
message: "Missing from node_modules",
|
|
779
|
+
fixed,
|
|
780
|
+
});
|
|
781
|
+
} else {
|
|
782
|
+
const fixed = options.fix ? await this.#removeOrphanedConfig(name) : false;
|
|
783
|
+
checks.push({
|
|
784
|
+
name: `orphan:${name}`,
|
|
785
|
+
status: "warning",
|
|
786
|
+
message: "Plugin in config but not installed",
|
|
787
|
+
fixed,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
769
790
|
} else {
|
|
770
791
|
checks.push({
|
|
771
792
|
name: `plugin:${name}`,
|
|
@@ -843,19 +864,6 @@ export class PluginManager {
|
|
|
843
864
|
}
|
|
844
865
|
}
|
|
845
866
|
|
|
846
|
-
// Check for orphaned runtime config entries
|
|
847
|
-
for (const name of Object.keys(config.plugins)) {
|
|
848
|
-
if (!(name in deps)) {
|
|
849
|
-
const fixed = options.fix ? await this.#removeOrphanedConfig(name) : false;
|
|
850
|
-
checks.push({
|
|
851
|
-
name: `orphan:${name}`,
|
|
852
|
-
status: "warning",
|
|
853
|
-
message: "Plugin in config but not installed",
|
|
854
|
-
fixed,
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
867
|
return checks;
|
|
860
868
|
}
|
|
861
869
|
|
|
@@ -192,8 +192,33 @@ export function parseMarketplaceCatalog(content: string, filePath: string): Mark
|
|
|
192
192
|
|
|
193
193
|
// ── fetchMarketplace ──────────────────────────────────────────────────
|
|
194
194
|
|
|
195
|
-
/**
|
|
196
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Catalog paths tried in priority order: omp-namespaced override first, then
|
|
197
|
+
* the Claude Code-compatible fallback so existing marketplaces keep loading.
|
|
198
|
+
*/
|
|
199
|
+
const CATALOG_RELATIVE_PATHS: readonly string[] = [
|
|
200
|
+
path.join(".omp-plugin", "marketplace.json"),
|
|
201
|
+
path.join(".claude-plugin", "marketplace.json"),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
async function readMarketplaceCatalog(root: string): Promise<{ catalogPath: string; content: string }> {
|
|
205
|
+
const tried: string[] = [];
|
|
206
|
+
for (const rel of CATALOG_RELATIVE_PATHS) {
|
|
207
|
+
const catalogPath = path.join(root, rel);
|
|
208
|
+
tried.push(catalogPath);
|
|
209
|
+
try {
|
|
210
|
+
const content = await Bun.file(catalogPath).text();
|
|
211
|
+
return { catalogPath, content };
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (isEnoent(err)) continue;
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Marketplace catalog not found at ${tried.map(p => `"${p}"`).join(" or ")}. ` +
|
|
219
|
+
`Ensure the directory exists and contains one of: ${CATALOG_RELATIVE_PATHS.join(", ")}.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
197
222
|
|
|
198
223
|
/**
|
|
199
224
|
* Expand a `~/...` path to an absolute path using os.homedir().
|
|
@@ -220,21 +245,7 @@ export async function fetchMarketplace(source: string, cacheDir: string): Promis
|
|
|
220
245
|
|
|
221
246
|
if (type === "local") {
|
|
222
247
|
const resolved = path.resolve(expandHome(source));
|
|
223
|
-
const catalogPath =
|
|
224
|
-
|
|
225
|
-
let content: string;
|
|
226
|
-
try {
|
|
227
|
-
content = await Bun.file(catalogPath).text();
|
|
228
|
-
} catch (err) {
|
|
229
|
-
if (isEnoent(err)) {
|
|
230
|
-
throw new Error(
|
|
231
|
-
`Marketplace catalog not found at "${catalogPath}". ` +
|
|
232
|
-
`Ensure the directory exists and contains a .claude-plugin/marketplace.json file.`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
throw err;
|
|
236
|
-
}
|
|
237
|
-
|
|
248
|
+
const { catalogPath, content } = await readMarketplaceCatalog(resolved);
|
|
238
249
|
const catalog = parseMarketplaceCatalog(content, catalogPath);
|
|
239
250
|
return { catalog };
|
|
240
251
|
}
|
|
@@ -280,27 +291,14 @@ async function cloneAndReadCatalog(url: string, cacheDir: string): Promise<Fetch
|
|
|
280
291
|
logger.debug(`[marketplace] cloning ${url} → ${tmpDir}`);
|
|
281
292
|
await git.clone(url, tmpDir);
|
|
282
293
|
|
|
283
|
-
const catalogPath = path.join(tmpDir, CATALOG_RELATIVE_PATH);
|
|
284
|
-
let content: string;
|
|
285
294
|
try {
|
|
286
|
-
content = await
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (isEnoent(err)) {
|
|
290
|
-
throw new Error(`Cloned repository has no marketplace catalog at ${CATALOG_RELATIVE_PATH}`);
|
|
291
|
-
}
|
|
292
|
-
throw err;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
let catalog: MarketplaceCatalog;
|
|
296
|
-
try {
|
|
297
|
-
catalog = parseMarketplaceCatalog(content, catalogPath);
|
|
295
|
+
const { catalogPath, content } = await readMarketplaceCatalog(tmpDir);
|
|
296
|
+
const catalog = parseMarketplaceCatalog(content, catalogPath);
|
|
297
|
+
return { catalog, clonePath: tmpDir };
|
|
298
298
|
} catch (err) {
|
|
299
299
|
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
300
|
-
throw err;
|
|
300
|
+
throw new Error(`Cloned repository ${url}: ${(err as Error).message}`, { cause: err });
|
|
301
301
|
}
|
|
302
|
-
|
|
303
|
-
return { catalog, clonePath: tmpDir };
|
|
304
302
|
}
|
|
305
303
|
|
|
306
304
|
/**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginRuntimeConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Normalizes persisted plugin runtime config across legacy lockfile shapes. */
|
|
4
|
+
export function normalizePluginRuntimeConfig(config: Partial<PluginRuntimeConfig>): PluginRuntimeConfig {
|
|
5
|
+
return {
|
|
6
|
+
plugins: config.plugins ?? {},
|
|
7
|
+
settings: config.settings ?? {},
|
|
8
|
+
};
|
|
9
|
+
}
|