mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-21 08:42:39 +00:00
* 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
81 lines
3.3 KiB
TypeScript
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;
|
|
}
|