javi-forge 1.38.4 → 1.38.5
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/dist/commands/plugin.js +10 -6
- package/dist/lib/plugin.d.ts +15 -1
- package/dist/lib/plugin.js +80 -15
- package/package.json +1 -1
package/dist/commands/plugin.js
CHANGED
|
@@ -62,16 +62,20 @@ export async function runPluginSearch(query, onStep) {
|
|
|
62
62
|
const stepId = "plugin-search";
|
|
63
63
|
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "running");
|
|
64
64
|
const results = await searchRegistry(query);
|
|
65
|
-
if (results.
|
|
66
|
-
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
if (results.status === "cancelled") {
|
|
66
|
+
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "error", "registry search cancelled");
|
|
67
|
+
}
|
|
68
|
+
else if (results.status === "unavailable") {
|
|
69
|
+
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "error", "registry unavailable");
|
|
70
|
+
}
|
|
71
|
+
else if (results.entries.length === 0) {
|
|
72
|
+
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "done", query ? `no plugins matching "${query}"` : "registry empty");
|
|
69
73
|
}
|
|
70
74
|
else {
|
|
71
|
-
const summary = results
|
|
75
|
+
const summary = results.entries
|
|
72
76
|
.map((p) => `${p.id} — ${p.description}`)
|
|
73
77
|
.join("\n ");
|
|
74
|
-
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "done", `${results.length} results:\n ${summary}`);
|
|
78
|
+
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "done", `${results.entries.length} results:\n ${summary}`);
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
/**
|
package/dist/lib/plugin.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { InstalledPlugin, PluginRegistryEntry, PluginSyncResult, PluginValidationResult } from "../types/index.js";
|
|
2
|
+
export type RegistrySearchResult = {
|
|
3
|
+
status: "success";
|
|
4
|
+
entries: PluginRegistryEntry[];
|
|
5
|
+
} | {
|
|
6
|
+
status: "unavailable";
|
|
7
|
+
} | {
|
|
8
|
+
status: "cancelled";
|
|
9
|
+
};
|
|
2
10
|
/**
|
|
3
11
|
* Validate a plugin directory structure and manifest.
|
|
4
12
|
*/
|
|
@@ -38,8 +46,14 @@ export declare function removePlugin(name: string, options?: {
|
|
|
38
46
|
export declare function listInstalledPlugins(): Promise<InstalledPlugin[]>;
|
|
39
47
|
/**
|
|
40
48
|
* Fetch the remote plugin registry and optionally filter by query.
|
|
49
|
+
*
|
|
50
|
+
* A registry that cannot be read or validated is deliberately distinct from a
|
|
51
|
+
* valid registry with no matches. Callers need that distinction to avoid
|
|
52
|
+
* presenting a network failure as a successful empty search.
|
|
41
53
|
*/
|
|
42
|
-
export declare function searchRegistry(query?: string
|
|
54
|
+
export declare function searchRegistry(query?: string, options?: {
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}): Promise<RegistrySearchResult>;
|
|
43
57
|
/**
|
|
44
58
|
* Detect installed plugins in a project's .javi-forge/plugins/ directory.
|
|
45
59
|
* Returns an array of plugin names (sorted alphabetically).
|
package/dist/lib/plugin.js
CHANGED
|
@@ -8,6 +8,7 @@ import { evaluateCoverageGate, scanFailureMessage, } from "./skill-install-gate.
|
|
|
8
8
|
import { scanSkillsWithCoverage } from "./skill-scanner.js";
|
|
9
9
|
const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
10
10
|
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
11
|
+
const REGISTRY_REQUEST_TIMEOUT_MS = 10_000;
|
|
11
12
|
// ── Validation ──────────────────────────────────────────────────────────────
|
|
12
13
|
/**
|
|
13
14
|
* Validate a plugin directory structure and manifest.
|
|
@@ -251,26 +252,90 @@ export async function listInstalledPlugins() {
|
|
|
251
252
|
}
|
|
252
253
|
/**
|
|
253
254
|
* Fetch the remote plugin registry and optionally filter by query.
|
|
255
|
+
*
|
|
256
|
+
* A registry that cannot be read or validated is deliberately distinct from a
|
|
257
|
+
* valid registry with no matches. Callers need that distinction to avoid
|
|
258
|
+
* presenting a network failure as a successful empty search.
|
|
254
259
|
*/
|
|
255
|
-
export async function searchRegistry(query) {
|
|
260
|
+
export async function searchRegistry(query, options = {}) {
|
|
261
|
+
const { signal: callerSignal } = options;
|
|
262
|
+
if (callerSignal?.aborted)
|
|
263
|
+
return { status: "cancelled" };
|
|
264
|
+
const controller = new AbortController();
|
|
265
|
+
let timedOut = false;
|
|
266
|
+
let callerCancelled = false;
|
|
267
|
+
let resolveDeadline;
|
|
268
|
+
const deadline = new Promise((resolve) => {
|
|
269
|
+
resolveDeadline = resolve;
|
|
270
|
+
});
|
|
271
|
+
const timeout = setTimeout(() => {
|
|
272
|
+
timedOut = true;
|
|
273
|
+
controller.abort();
|
|
274
|
+
resolveDeadline();
|
|
275
|
+
}, REGISTRY_REQUEST_TIMEOUT_MS);
|
|
276
|
+
const cancelFromCaller = () => {
|
|
277
|
+
callerCancelled = true;
|
|
278
|
+
controller.abort();
|
|
279
|
+
resolveDeadline();
|
|
280
|
+
};
|
|
281
|
+
callerSignal?.addEventListener("abort", cancelFromCaller, { once: true });
|
|
256
282
|
try {
|
|
257
|
-
const response = await
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
283
|
+
const response = await Promise.race([
|
|
284
|
+
Promise.resolve().then(() => fetch(PLUGIN_REGISTRY_URL, { signal: controller.signal })),
|
|
285
|
+
deadline,
|
|
286
|
+
]);
|
|
287
|
+
if (callerCancelled || callerSignal?.aborted)
|
|
288
|
+
return { status: "cancelled" };
|
|
289
|
+
if (timedOut || !response || !response.ok)
|
|
290
|
+
return { status: "unavailable" };
|
|
291
|
+
const body = await Promise.race([response.json(), deadline]);
|
|
292
|
+
if (callerCancelled || callerSignal?.aborted)
|
|
293
|
+
return { status: "cancelled" };
|
|
294
|
+
if (timedOut || !isPluginRegistry(body))
|
|
295
|
+
return { status: "unavailable" };
|
|
296
|
+
const entries = query
|
|
297
|
+
? filterRegistryEntries(body.plugins, query)
|
|
298
|
+
: body.plugins;
|
|
299
|
+
return { status: "success", entries };
|
|
270
300
|
}
|
|
271
301
|
catch {
|
|
272
|
-
return
|
|
302
|
+
return callerCancelled || callerSignal?.aborted
|
|
303
|
+
? { status: "cancelled" }
|
|
304
|
+
: { status: "unavailable" };
|
|
273
305
|
}
|
|
306
|
+
finally {
|
|
307
|
+
clearTimeout(timeout);
|
|
308
|
+
callerSignal?.removeEventListener("abort", cancelFromCaller);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function filterRegistryEntries(plugins, query) {
|
|
312
|
+
const normalizedQuery = query.toLowerCase();
|
|
313
|
+
return plugins.filter((plugin) => plugin.id.toLowerCase().includes(normalizedQuery) ||
|
|
314
|
+
plugin.description.toLowerCase().includes(normalizedQuery) ||
|
|
315
|
+
plugin.tags.some((tag) => tag.toLowerCase().includes(normalizedQuery)));
|
|
316
|
+
}
|
|
317
|
+
function isPluginRegistry(value) {
|
|
318
|
+
if (!isRecord(value))
|
|
319
|
+
return false;
|
|
320
|
+
return (typeof value.version === "string" &&
|
|
321
|
+
typeof value.updatedAt === "string" &&
|
|
322
|
+
Array.isArray(value.plugins) &&
|
|
323
|
+
value.plugins.every(isPluginRegistryEntry));
|
|
324
|
+
}
|
|
325
|
+
function isPluginRegistryEntry(value) {
|
|
326
|
+
if (!isRecord(value))
|
|
327
|
+
return false;
|
|
328
|
+
return (typeof value.id === "string" &&
|
|
329
|
+
typeof value.repository === "string" &&
|
|
330
|
+
typeof value.description === "string" &&
|
|
331
|
+
Array.isArray(value.tags) &&
|
|
332
|
+
value.tags.every((tag) => typeof tag === "string") &&
|
|
333
|
+
(value.stars === undefined ||
|
|
334
|
+
(typeof value.stars === "number" && Number.isFinite(value.stars))) &&
|
|
335
|
+
(value.updatedAt === undefined || typeof value.updatedAt === "string"));
|
|
336
|
+
}
|
|
337
|
+
function isRecord(value) {
|
|
338
|
+
return typeof value === "object" && value !== null;
|
|
274
339
|
}
|
|
275
340
|
// ── Sync ───────────────────────────────────────────────────────────────
|
|
276
341
|
/**
|