mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
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
This commit is contained in:
@@ -414,7 +414,6 @@ export interface PaseoDaemonConfig {
|
||||
plugins?: Record<string, PluginSource>;
|
||||
pluginRegistries?: PluginRegistries;
|
||||
pluginRegistryUrl?: string;
|
||||
pluginRegistryEnabled?: boolean;
|
||||
staticDir: string;
|
||||
mcpDebug: boolean;
|
||||
isDev?: boolean;
|
||||
@@ -625,7 +624,6 @@ export async function createPaseoDaemon(
|
||||
usageSession: (id) => agentManager.usageSession(id),
|
||||
},
|
||||
managedSources: new ManagedPluginSources(config.paseoHome, {
|
||||
enabled: config.pluginRegistryEnabled ?? false,
|
||||
registries: config.pluginRegistries,
|
||||
defaultUrl: config.pluginRegistryUrl,
|
||||
}),
|
||||
|
||||
@@ -30,7 +30,6 @@ export const DAEMON_SETTING_ENV_KEYS = [
|
||||
"PASEO_LOG_ROTATE_SIZE",
|
||||
"PASEO_PASSWORD",
|
||||
"PASEO_PLUGIN_REGISTRY",
|
||||
"PASEO_PLUGIN_REGISTRY_ENABLED",
|
||||
"PASEO_RELAY_ENABLED",
|
||||
"PASEO_RELAY_ENDPOINT",
|
||||
"PASEO_RELAY_PUBLIC_ENDPOINT",
|
||||
|
||||
@@ -205,21 +205,19 @@ test("loads private plugin registry settings through the configuration boundary"
|
||||
const home = await mkdtemp(path.join(os.tmpdir(), "paseo-registry-config-"));
|
||||
try {
|
||||
const pluginRegistries = { "plugins.example.test": { authorization: "Bearer fixture" } };
|
||||
await writeFile(path.join(home, "config.json"), JSON.stringify({ pluginRegistries }));
|
||||
await writeFile(
|
||||
path.join(home, "config.json"),
|
||||
JSON.stringify({ pluginRegistries, pluginRegistryEnabled: false }),
|
||||
);
|
||||
const config = loadConfig(home, {
|
||||
env: { PASEO_PLUGIN_REGISTRY: "https://plugins.example.test/internal" },
|
||||
env: {
|
||||
PASEO_PLUGIN_REGISTRY: "https://plugins.example.test/internal",
|
||||
PASEO_PLUGIN_REGISTRY_ENABLED: "false",
|
||||
},
|
||||
});
|
||||
expect(config.pluginRegistryUrl).toBe("https://plugins.example.test/internal");
|
||||
expect(config.pluginRegistries).toEqual(pluginRegistries);
|
||||
expect(config.pluginRegistryEnabled).toBe(false);
|
||||
await writeFile(
|
||||
path.join(home, "config.json"),
|
||||
JSON.stringify({ pluginRegistries, pluginRegistryEnabled: true }),
|
||||
);
|
||||
expect(loadConfig(home, { env: {} }).pluginRegistryEnabled).toBe(true);
|
||||
expect(
|
||||
loadConfig(home, { env: { PASEO_PLUGIN_REGISTRY_ENABLED: "false" } }).pluginRegistryEnabled,
|
||||
).toBe(false);
|
||||
expect(config).not.toHaveProperty("pluginRegistryEnabled");
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -628,10 +628,6 @@ export function resolveConfigFromPersisted(
|
||||
plugins: persisted.plugins,
|
||||
pluginRegistries: persisted.pluginRegistries,
|
||||
pluginRegistryUrl: env.PASEO_PLUGIN_REGISTRY,
|
||||
pluginRegistryEnabled:
|
||||
parseBooleanEnv(env.PASEO_PLUGIN_REGISTRY_ENABLED) ??
|
||||
persisted.pluginRegistryEnabled ??
|
||||
false,
|
||||
mcpDebug: env.MCP_DEBUG === "1",
|
||||
isDev: resolvePaseoNodeEnv(env) === "development",
|
||||
agentStoragePath: path.join(paseoHome, "agents"),
|
||||
|
||||
@@ -308,6 +308,7 @@ export const PersistedConfigSchema = z
|
||||
|
||||
providers: ProvidersSchema.optional(),
|
||||
pluginRegistries: PluginRegistriesSchema.optional(),
|
||||
// COMPAT(plugin-registry-gate): added in v0.11.0; accept the ignored startup key until 2027-04-06.
|
||||
pluginRegistryEnabled: z.boolean().optional(),
|
||||
pluginsEnabled: z.boolean().optional(),
|
||||
plugins: z.record(PluginIdSchema, PluginSourceSchema).optional(),
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
stat,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -451,7 +452,102 @@ describe("PluginService", () => {
|
||||
await service.stopAllPlugins();
|
||||
});
|
||||
|
||||
it("prefers an existing directory and installs its selected plugin subdirectory", async () => {
|
||||
it("resolves a bare registry id even when a matching directory exists in the daemon cwd", async () => {
|
||||
const home = await mkdtemp(path.join(tmpdir(), "paseo-plugin-home-"));
|
||||
const owner = path.join(process.cwd(), `registry-shadow-${randomUUID()}`);
|
||||
await mkdir(owner);
|
||||
roots.push(home, owner);
|
||||
const source = `${path.basename(owner)}/example`;
|
||||
const repository = await createPlugin("registry-example", "export default () => () => {};\n");
|
||||
await mkdir(path.join(owner, "example"));
|
||||
await cp(repository, path.join(owner, "example"), { recursive: true });
|
||||
await runGitCommand(["init", "-b", "main"], { cwd: repository });
|
||||
await runGitCommand(["add", "-A"], { cwd: repository });
|
||||
await runGitCommand(
|
||||
[
|
||||
"-c",
|
||||
"user.name=Paseo Tests",
|
||||
"-c",
|
||||
"user.email=paseo@example.test",
|
||||
"commit",
|
||||
"-m",
|
||||
"fixture",
|
||||
],
|
||||
{ cwd: repository },
|
||||
);
|
||||
const { stdout } = await runGitCommand(["rev-parse", "HEAD"], { cwd: repository });
|
||||
const requests: Array<string | undefined> = [];
|
||||
const server = createServer((request, response) => {
|
||||
requests.push(request.url);
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
id: source,
|
||||
name: "Example",
|
||||
description: "Test",
|
||||
categories: [],
|
||||
author: { github: path.basename(owner) },
|
||||
repository: { url: "https://github.com/fixture/example" },
|
||||
artifact: {
|
||||
kind: "git",
|
||||
remote: pathToFileURL(repository).href,
|
||||
commit: stdout.trim(),
|
||||
pluginPath: ".",
|
||||
},
|
||||
screenshots: [],
|
||||
submittedAt: "2026-10-03",
|
||||
reviewedAt: "2026-10-03",
|
||||
updatedAt: "2026-10-03",
|
||||
publishedAt: "2026-10-03",
|
||||
readme: "# Test",
|
||||
}),
|
||||
);
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("Missing registry address");
|
||||
const url = `http://127.0.0.1:${address.port}`;
|
||||
const store = createStore(home);
|
||||
store.patch({ pluginsEnabled: false });
|
||||
const service = bindTestSessionHost(
|
||||
new PluginService(pino({ level: "silent" }), store, "0.4.0", {
|
||||
managedSources: new ManagedPluginSources(home, { defaultUrl: url }),
|
||||
}),
|
||||
);
|
||||
try {
|
||||
await service.start();
|
||||
await expect(service.installSource({ source })).resolves.toMatchObject({
|
||||
id: "registry-example",
|
||||
status: "disabled",
|
||||
installation: {
|
||||
identity: { kind: "git", registry: { url, id: source } },
|
||||
currentRevision: stdout.trim(),
|
||||
},
|
||||
});
|
||||
expect(requests).toEqual([`/plugins/${source}.json`]);
|
||||
await expect(
|
||||
service.installSource({ source: `./${source}`, id: "local-example" }),
|
||||
).resolves.toMatchObject({
|
||||
installation: { identity: { kind: "directory", path: path.join(owner, "example") } },
|
||||
});
|
||||
expect(requests).toEqual([`/plugins/${source}.json`]);
|
||||
} finally {
|
||||
await service.stopAllPlugins();
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it("names a missing explicit directory instead of trying a managed source", async () => {
|
||||
const home = await mkdtemp(path.join(tmpdir(), "paseo-plugin-home-"));
|
||||
roots.push(home);
|
||||
const directory = path.join(home, "missing");
|
||||
const service = createService(home);
|
||||
await expect(service.installSource({ source: directory })).rejects.toThrow(
|
||||
`Plugin directory does not exist: ${directory}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("installs an explicit directory and its selected plugin subdirectory", async () => {
|
||||
const home = await mkdtemp(path.join(tmpdir(), "paseo-plugin-home-"));
|
||||
roots.push(home);
|
||||
const repository = await mkdtemp(path.join(tmpdir(), "owner-repository-"));
|
||||
|
||||
@@ -21,6 +21,7 @@ import { BUILTIN_PROVIDER_IDS } from "@getpaseo/protocol/provider-manifest";
|
||||
import type { DaemonConfigStore } from "../daemon-config-store.js";
|
||||
import { type ManagedPluginCandidate, ManagedPluginSources } from "./managed-source.js";
|
||||
import { readPluginManifest } from "./manifest.js";
|
||||
import { expandTilde } from "../../utils/path.js";
|
||||
import { runPluginBuild } from "./preparation.js";
|
||||
import { PluginRuntime } from "./runtime.js";
|
||||
import { BuiltinPluginLoader, type BuiltinPlugin } from "./builtin/index.js";
|
||||
@@ -286,16 +287,11 @@ export class PluginService {
|
||||
if (input.id && this.builtinPluginIds.has(input.id)) {
|
||||
throw new Error(`Plugin ID "${input.id}" is reserved for a built-in plugin`);
|
||||
}
|
||||
const directDirectory = path.resolve(input.source);
|
||||
const explicit = /^(npm:|github:|git:(?!\/\/))/.test(input.source);
|
||||
const directInfo = await stat(directDirectory).catch(() => null);
|
||||
const reference = directInfo?.isDirectory()
|
||||
? { source: input.source, pluginPath: undefined }
|
||||
: parsePluginSourceReference(input.source);
|
||||
const directory = path.resolve(reference.source);
|
||||
let info = directInfo;
|
||||
if (!info?.isDirectory() && !explicit) info = await stat(directory).catch(() => null);
|
||||
if (info?.isDirectory()) {
|
||||
const reference = parsePluginSourceReference(input.source);
|
||||
if (reference.kind === "directory") {
|
||||
const directory = path.resolve(expandTilde(reference.source));
|
||||
const info = await stat(directory).catch(() => null);
|
||||
if (!info?.isDirectory()) throw new Error(`Plugin directory does not exist: ${directory}`);
|
||||
if (input.ref) throw new Error("Plugin --ref is only valid for Git sources");
|
||||
const pluginDirectory = resolveLocalPluginPath(directory, reference.pluginPath);
|
||||
return this.installDirectory({ path: pluginDirectory, id: input.id });
|
||||
|
||||
@@ -280,7 +280,60 @@ describe("managed Git plugin sources", () => {
|
||||
});
|
||||
|
||||
describe("registry plugin sources", () => {
|
||||
it("treats owner/repository as GitHub shorthand without contacting the registry while registry installs are off", async () => {
|
||||
it("explains how to choose a revision when --ref is used with a registry id", async () => {
|
||||
const home = await mkdtemp(path.join(tmpdir(), "paseo-registry-ref-home-"));
|
||||
roots.push(home);
|
||||
const sources = new ManagedPluginSources(home);
|
||||
await expect(
|
||||
sources.prepareInstall({ source: "fixture/example", ref: "main" }),
|
||||
).rejects.toThrow(
|
||||
"Registry installs pin the reviewed revision. Drop --ref, or install from an explicit github:owner/repository source to choose a revision.",
|
||||
);
|
||||
});
|
||||
|
||||
it("names the registry URL and recovery steps when the registry fetch fails", async () => {
|
||||
const home = await mkdtemp(path.join(tmpdir(), "paseo-registry-offline-home-"));
|
||||
roots.push(home);
|
||||
const server = createServer((request) => request.socket.destroy());
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("Missing registry address");
|
||||
const url = `http://127.0.0.1:${address.port}`;
|
||||
try {
|
||||
const sources = new ManagedPluginSources(home, { defaultUrl: url });
|
||||
await expect(sources.prepareInstall({ source: "fixture/example" })).rejects.toThrow(
|
||||
`Could not reach plugin registry ${url}/plugins/fixture/example.json. Plugin fixture/example was not installed. Retry or use an explicit npm: or github: source.`,
|
||||
);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
it("explains explicit local and GitHub sources when a registry ID is missing", async () => {
|
||||
const home = await mkdtemp(path.join(tmpdir(), "paseo-registry-missing-home-"));
|
||||
roots.push(home);
|
||||
const requests: Array<string | undefined> = [];
|
||||
const server = createServer((request, response) => {
|
||||
requests.push(request.url);
|
||||
response.writeHead(404).end();
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("Missing registry address");
|
||||
try {
|
||||
const sources = new ManagedPluginSources(home, {
|
||||
defaultUrl: `http://127.0.0.1:${address.port}`,
|
||||
});
|
||||
await expect(sources.prepareInstall({ source: "fixture/missing" })).rejects.toThrow(
|
||||
"If you intended a local directory, use ./fixture/missing. If you intended a GitHub source, use git:fixture/missing, or a full Git URL for another Git host.",
|
||||
);
|
||||
expect(requests).toEqual(["/plugins/fixture/missing.json"]);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps explicit Git sources and persisted Git installs independent of the registry", async () => {
|
||||
const repository = await createRepository();
|
||||
const home = await mkdtemp(path.join(tmpdir(), "paseo-registry-off-home-"));
|
||||
roots.push(home);
|
||||
@@ -298,12 +351,34 @@ describe("registry plugin sources", () => {
|
||||
const sources = new ManagedPluginSources(home, {
|
||||
defaultUrl: `http://127.0.0.1:${address.port}`,
|
||||
});
|
||||
const candidate = await sources.prepareInstall({ source: "fixture/repository" });
|
||||
expect(candidate.record).toEqual({
|
||||
kind: "git",
|
||||
remote: "https://github.com/fixture/repository.git",
|
||||
});
|
||||
await sources.discard(candidate);
|
||||
for (const source of [
|
||||
"git:fixture/repository",
|
||||
"https://github.com/fixture/repository.git",
|
||||
]) {
|
||||
const candidate = await sources.place(
|
||||
"managed-example",
|
||||
await sources.prepareInstall({ source }),
|
||||
);
|
||||
expect(candidate.record).toEqual({
|
||||
kind: "git",
|
||||
remote: "https://github.com/fixture/repository.git",
|
||||
});
|
||||
sources.commit("managed-example", candidate.record);
|
||||
const restarted = new ManagedPluginSources(home, {
|
||||
defaultUrl: `http://127.0.0.1:${address.port}/changed`,
|
||||
});
|
||||
expect((await restarted.preview("managed-example", candidate.directory)).outcome).toBe(
|
||||
"current",
|
||||
);
|
||||
expect(
|
||||
(await restarted.describe("managed-example", candidate.directory)).identity,
|
||||
).toEqual({
|
||||
kind: "git",
|
||||
remote: "https://github.com/fixture/repository.git",
|
||||
pluginPath: ".",
|
||||
});
|
||||
await sources.discard(candidate);
|
||||
}
|
||||
});
|
||||
expect(requests).toEqual([]);
|
||||
} finally {
|
||||
@@ -332,12 +407,17 @@ describe("registry plugin sources", () => {
|
||||
intent: string | string[] | undefined;
|
||||
authorization: string | undefined;
|
||||
}> = [];
|
||||
let missing = false;
|
||||
const server = createServer((req, res) => {
|
||||
requests.push({
|
||||
path: req.url,
|
||||
intent: req.headers["x-paseo-install"],
|
||||
authorization: req.headers.authorization,
|
||||
});
|
||||
if (missing) {
|
||||
res.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
@@ -369,7 +449,6 @@ describe("registry plugin sources", () => {
|
||||
const url = `http://${host}/internal`;
|
||||
try {
|
||||
const sources = new ManagedPluginSources(home, {
|
||||
enabled: true,
|
||||
defaultUrl: url,
|
||||
registries: { [host]: { authorization: "Bearer test" } },
|
||||
});
|
||||
@@ -380,7 +459,11 @@ describe("registry plugin sources", () => {
|
||||
).toBe("managed-example");
|
||||
await sources.verifyCandidate("managed-example", candidate);
|
||||
sources.commit("managed-example", candidate.record);
|
||||
const preview = await sources.preview("managed-example", candidate.directory);
|
||||
const restarted = new ManagedPluginSources(home, {
|
||||
defaultUrl: "http://127.0.0.1:1/changed",
|
||||
registries: { [host]: { authorization: "Bearer test" } },
|
||||
});
|
||||
const preview = await restarted.preview("managed-example", candidate.directory);
|
||||
expect(preview.outcome).toBe("current");
|
||||
expect(preview.current?.identity).toEqual({
|
||||
kind: "git",
|
||||
@@ -388,6 +471,10 @@ describe("registry plugin sources", () => {
|
||||
pluginPath: "packages/example",
|
||||
registry: { url, id: "acme/example" },
|
||||
});
|
||||
missing = true;
|
||||
await expect(restarted.preview("managed-example", candidate.directory)).rejects.toThrow(
|
||||
"Check that the installed plugin is still published in this registry before updating",
|
||||
);
|
||||
expect(requests).toEqual([
|
||||
{ path: "/internal/plugins/acme/example.json", intent: "1", authorization: "Bearer test" },
|
||||
{
|
||||
@@ -395,6 +482,11 @@ describe("registry plugin sources", () => {
|
||||
intent: undefined,
|
||||
authorization: "Bearer test",
|
||||
},
|
||||
{
|
||||
path: "/internal/plugins/acme/example.json",
|
||||
intent: undefined,
|
||||
authorization: "Bearer test",
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
@@ -444,7 +536,7 @@ it("installs and updates only the npm artifacts pinned by the plugin registry",
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("Missing registry address");
|
||||
const options = { enabled: true, defaultUrl: `http://127.0.0.1:${address.port}` };
|
||||
const options = { defaultUrl: `http://127.0.0.1:${address.port}` };
|
||||
try {
|
||||
const sources = new ManagedPluginSources(home, options);
|
||||
const candidate = await sources.place(
|
||||
|
||||
@@ -147,11 +147,13 @@ export class ManagedPluginSources {
|
||||
input: InstallInput,
|
||||
target?: PluginUpdateTarget,
|
||||
): Promise<ManagedPluginCandidate> {
|
||||
const registry = this.registryOptions.enabled
|
||||
? parsePluginRegistryReference(input.source, this.registryOptions.defaultUrl)
|
||||
: null;
|
||||
const registry = parsePluginRegistryReference(input.source, this.registryOptions.defaultUrl);
|
||||
if (registry) {
|
||||
if (input.ref || input.pluginPath)
|
||||
if (input.ref)
|
||||
throw new Error(
|
||||
"Registry installs pin the reviewed revision. Drop --ref, or install from an explicit github:owner/repository source to choose a revision.",
|
||||
);
|
||||
if (input.pluginPath)
|
||||
throw new Error("Registry sources use their reviewed artifact path and revision");
|
||||
const resolved = await resolveRegistryPlugin(registry, this.registryOptions, true);
|
||||
const candidate = await this.prepareInstall(resolved.input, resolved.target);
|
||||
@@ -175,7 +177,7 @@ export class ManagedPluginSources {
|
||||
sourceRoot = path.join(versionRoot, "node_modules", artifact.packageName);
|
||||
record = { kind: "npm" };
|
||||
} else {
|
||||
const remote = normalizeGitSource(input.source, !this.registryOptions.enabled);
|
||||
const remote = normalizeGitSource(input.source);
|
||||
sourceRoot = path.join(versionRoot, "checkout");
|
||||
await clone(remote, sourceRoot);
|
||||
const commit = await resolveRequestedRef(sourceRoot, input.ref);
|
||||
@@ -227,7 +229,7 @@ export class ManagedPluginSources {
|
||||
const location = this.locate(pluginId, configuredPath);
|
||||
let target: PluginUpdateTarget;
|
||||
let outcome: PluginUpdatePreview["outcome"];
|
||||
if (this.registryOptions.enabled && current.identity.registry) {
|
||||
if (current.identity.registry) {
|
||||
if (selection) throw new Error("Registry updates use only the reviewed registry pin");
|
||||
const resolved = await resolveRegistryPlugin(
|
||||
current.identity.registry,
|
||||
@@ -304,7 +306,7 @@ export class ManagedPluginSources {
|
||||
await this.assertCurrent(proposal, configuredPath);
|
||||
const identity = this.locate(proposal.id, configuredPath).identity;
|
||||
const record = this.records[proposal.id];
|
||||
if (this.registryOptions.enabled && identity.kind !== "directory" && identity.registry) {
|
||||
if (identity.kind !== "directory" && identity.registry) {
|
||||
const resolved = await resolveRegistryPlugin(identity.registry, this.registryOptions, false);
|
||||
assertRegistryArtifactIdentity(identity, resolved.artifact);
|
||||
if (!isDeepStrictEqual(resolved.target, proposal.target))
|
||||
@@ -436,10 +438,9 @@ function reviewLinks(current: PluginInstallation, target: PluginUpdateTarget): s
|
||||
}
|
||||
return [];
|
||||
}
|
||||
/** Bare owner/repo is GitHub shorthand unless registry references own that syntax. */
|
||||
function normalizeGitSource(source: string, bareGitHubShorthand: boolean): string {
|
||||
const expandsShorthand =
|
||||
bareGitHubShorthand || source.startsWith("github:") || source.startsWith("git:");
|
||||
/** Only explicit Git prefixes expand GitHub shorthand. */
|
||||
function normalizeGitSource(source: string): string {
|
||||
const expandsShorthand = source.startsWith("github:") || source.startsWith("git:");
|
||||
if (source.startsWith("github:")) {
|
||||
const shorthand = source.slice(7);
|
||||
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(shorthand)) {
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
import type { PluginUpdateTarget } from "@getpaseo/protocol/messages";
|
||||
|
||||
export interface RegistryOptions {
|
||||
/** Off: bare owner/repo is GitHub shorthand and no registry is contacted. */
|
||||
enabled?: boolean;
|
||||
defaultUrl?: string;
|
||||
registries?: PluginRegistries;
|
||||
}
|
||||
@@ -29,11 +27,26 @@ export async function resolveRegistryPlugin(
|
||||
const headers: Record<string, string> = {};
|
||||
if (authorization) headers.Authorization = authorization;
|
||||
if (install) headers["X-Paseo-Install"] = "1";
|
||||
const response = await fetch(`${identity.url.replace(/\/+$/, "")}/plugins/${identity.id}.json`, {
|
||||
headers,
|
||||
redirect: "error",
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
const url = `${identity.url.replace(/\/+$/, "")}/plugins/${identity.id}.json`;
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
headers,
|
||||
redirect: "error",
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
} catch (cause) {
|
||||
const guidance = install
|
||||
? `Plugin ${identity.id} was not installed. Retry or use an explicit npm: or github: source.`
|
||||
: `Could not check updates for ${identity.id}. Retry later.`;
|
||||
throw new Error(`Could not reach plugin registry ${url}. ${guidance}`, { cause });
|
||||
}
|
||||
if (response.status === 404) {
|
||||
const guidance = install
|
||||
? `If you intended a local directory, use ./${identity.id}. If you intended a GitHub source, use git:${identity.id}, or a full Git URL for another Git host.`
|
||||
: "Check that the installed plugin is still published in this registry before updating.";
|
||||
throw new Error(`Plugin ${identity.id} was not found in registry ${base.host}. ${guidance}`);
|
||||
}
|
||||
if (!response.ok)
|
||||
throw new Error(`Registry ${base.host} returned ${response.status} for ${identity.id}`);
|
||||
const plugin = PublishedPluginDetailSchema.parse(await response.json());
|
||||
|
||||
Reference in New Issue
Block a user