javi-forge 1.38.4 → 1.38.6
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.d.ts +3 -1
- package/dist/commands/plugin.js +12 -8
- package/dist/lib/plugin.d.ts +15 -1
- package/dist/lib/plugin.js +80 -15
- package/dist/ui/Plugin.js +31 -1
- package/package.json +1 -1
|
@@ -17,7 +17,9 @@ export declare function runPluginList(onStep: StepCallback): Promise<void>;
|
|
|
17
17
|
/**
|
|
18
18
|
* Search the remote plugin registry.
|
|
19
19
|
*/
|
|
20
|
-
export declare function runPluginSearch(query: string | undefined, onStep: StepCallback
|
|
20
|
+
export declare function runPluginSearch(query: string | undefined, onStep: StepCallback, options?: {
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}): Promise<void>;
|
|
21
23
|
/**
|
|
22
24
|
* Validate a local plugin directory.
|
|
23
25
|
*/
|
package/dist/commands/plugin.js
CHANGED
|
@@ -58,20 +58,24 @@ export async function runPluginList(onStep) {
|
|
|
58
58
|
/**
|
|
59
59
|
* Search the remote plugin registry.
|
|
60
60
|
*/
|
|
61
|
-
export async function runPluginSearch(query, onStep) {
|
|
61
|
+
export async function runPluginSearch(query, onStep, options = {}) {
|
|
62
62
|
const stepId = "plugin-search";
|
|
63
63
|
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "running");
|
|
64
|
-
const results = await searchRegistry(query);
|
|
65
|
-
if (results.
|
|
66
|
-
report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
const results = await searchRegistry(query, options);
|
|
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
|
/**
|
package/dist/ui/Plugin.js
CHANGED
|
@@ -19,6 +19,15 @@ const STATUS_COLOR = {
|
|
|
19
19
|
export default function Plugin({ action, target, dryRun, codex = false, force = false, }) {
|
|
20
20
|
const [steps, setSteps] = useState([]);
|
|
21
21
|
const [done, setDone] = useState(false);
|
|
22
|
+
const handleTerminalSignal = (signal, signalController) => {
|
|
23
|
+
if (signalController.signal.aborted)
|
|
24
|
+
return;
|
|
25
|
+
signalController.abort();
|
|
26
|
+
if (signal === "SIGINT")
|
|
27
|
+
process.exitCode = 130;
|
|
28
|
+
if (signal === "SIGTERM")
|
|
29
|
+
process.exitCode = 143;
|
|
30
|
+
};
|
|
22
31
|
const onStep = (step) => {
|
|
23
32
|
setSteps((prev) => {
|
|
24
33
|
const idx = prev.findIndex((s) => s.id === step.id);
|
|
@@ -31,6 +40,17 @@ export default function Plugin({ action, target, dryRun, codex = false, force =
|
|
|
31
40
|
});
|
|
32
41
|
};
|
|
33
42
|
useEffect(() => {
|
|
43
|
+
const controller = action === "search" ? new AbortController() : undefined;
|
|
44
|
+
const sigintHandler = controller === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: () => handleTerminalSignal("SIGINT", controller);
|
|
47
|
+
const sigtermHandler = controller === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: () => handleTerminalSignal("SIGTERM", controller);
|
|
50
|
+
if (action === "search" && sigintHandler && sigtermHandler) {
|
|
51
|
+
process.on("SIGINT", sigintHandler);
|
|
52
|
+
process.on("SIGTERM", sigtermHandler);
|
|
53
|
+
}
|
|
34
54
|
const run = async () => {
|
|
35
55
|
try {
|
|
36
56
|
switch (action) {
|
|
@@ -62,7 +82,9 @@ export default function Plugin({ action, target, dryRun, codex = false, force =
|
|
|
62
82
|
await runPluginList(onStep);
|
|
63
83
|
break;
|
|
64
84
|
case "search":
|
|
65
|
-
await runPluginSearch(target, onStep
|
|
85
|
+
await runPluginSearch(target, onStep, {
|
|
86
|
+
signal: controller?.signal,
|
|
87
|
+
});
|
|
66
88
|
break;
|
|
67
89
|
case "validate":
|
|
68
90
|
if (!target) {
|
|
@@ -129,6 +151,14 @@ export default function Plugin({ action, target, dryRun, codex = false, force =
|
|
|
129
151
|
setDone(true);
|
|
130
152
|
};
|
|
131
153
|
run();
|
|
154
|
+
return () => {
|
|
155
|
+
if (sigintHandler) {
|
|
156
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
157
|
+
}
|
|
158
|
+
if (sigtermHandler) {
|
|
159
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
132
162
|
}, [action, target, dryRun, force]);
|
|
133
163
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
134
164
|
React.createElement(Box, { marginBottom: 1 },
|