Files
paseo/packages/protocol/src/plugin-source-reference.ts
Mohamed Boudra 7fd469ae19 Enable plugin registry installs by default (#6224)
* feat(plugins): enable registry installs by default

* fix(plugins): explain explicit Git source for missing registry IDs

* fix(plugins): tailor missing registry guidance to installation

* test(app): sample footer geometry in one browser frame

* fix(plugins): make registry source resolution deterministic

Directory existence must not change the meaning of a registry identifier shared by CLI, RPC, and UI installs. Classify explicit host paths in the source parser and explain recovery from registry transport and revision-selection failures.

* fix(plugins): recognize dot and Windows directory sources

* test(cli): wait for readable daemon ownership before cancellation

The cancellation test raced PID-file creation against its first write and parsed empty JSON in CI. Use the public daemon-instance reader to wait for and capture the supervisor PID before asserting cancellation cleanup.

* test(app): expect missing directory install guidance
2026-10-06 16:52:02 +01:00

81 lines
3.3 KiB
TypeScript

import { parsePluginRegistryReference } from "./plugin-registry.js";
import type { PluginInstallation } from "./messages.js";
export interface PluginSourceReference {
kind: "directory" | "managed";
source: string;
pluginPath: string | undefined;
}
export function parsePluginSourceReference(reference: string): PluginSourceReference {
const prefix = /^(npm:|github:|git:(?!\/\/))/.exec(reference)?.[0];
const source = prefix ? reference.slice(prefix.length) : reference;
const parsed = splitPluginPath(source, { scp: prefix !== "npm:" });
if (!prefix && /^(\.\.?(?:[/\\]|$)|\/|~|[A-Za-z]:[/\\]|\\\\)/.test(parsed.source)) {
return { kind: "directory", ...parsed };
}
if (parsePluginRegistryReference(reference))
return { kind: "managed", source: reference, pluginPath: undefined };
return { kind: "managed", ...parsed, source: `${prefix ?? ""}${parsed.source}` };
}
function splitPluginPath(
reference: string,
options: { scp: boolean },
): Pick<PluginSourceReference, "source" | "pluginPath"> {
const separator = reference.lastIndexOf(":");
if (separator === -1) return { source: reference, pluginPath: undefined };
const pluginPath = reference.slice(separator + 1);
if (!isPortableRelativePluginPath(pluginPath)) {
return { source: reference, pluginPath: undefined };
}
const scheme = reference.indexOf("://");
if (scheme !== -1) {
const pathStart = reference.indexOf("/", scheme + 3);
if (pathStart === -1 || separator < pathStart) {
return { source: reference, pluginPath: undefined };
}
} else if (options.scp) {
const scpSeparator = reference.match(/^[^/@\s]+@[^:\s]+:/)?.[0].length;
if (scpSeparator !== undefined && separator === scpSeparator - 1) {
return { source: reference, pluginPath: undefined };
}
}
return { source: reference.slice(0, separator), pluginPath };
}
export function formatPluginSourceReference(
source: string,
pluginPath: string | undefined,
): string {
if (pluginPath === undefined) return source;
if (!isPortableRelativePluginPath(pluginPath)) {
throw new Error("Plugin path must be a portable path relative to the repository");
}
return `${source}:${pluginPath}`;
}
function isPortableRelativePluginPath(pluginPath: string): boolean {
if (pluginPath === ".") return true;
if (!pluginPath || pluginPath.startsWith("/") || pluginPath.startsWith("\\")) return false;
if (/^[A-Za-z]:/.test(pluginPath) || pluginPath.includes(":")) return false;
return pluginPath.split(/[\\/]/).every((part) => part !== "" && part !== "." && part !== "..");
}
export function formatPluginIdentity(identity: PluginInstallation["identity"]): string {
if (identity.kind === "directory") return identity.path;
if (identity.registry) return `${new URL(identity.registry.url).host}/${identity.registry.id}`;
const source = identity.kind === "npm" ? `npm:${identity.packageName}` : `git:${identity.remote}`;
return identity.pluginPath === "." ? source : `${source}:${identity.pluginPath}`;
}
export function formatPluginInstallation(installation: PluginInstallation): string {
const identity = formatPluginIdentity(installation.identity);
const revision =
installation.identity.kind === "git"
? installation.currentRevision?.slice(0, 12)
: installation.currentRevision;
return revision ? `${identity} · ${revision}` : identity;
}