fix(update-check): resolve the release URL for a relative plugin source
This project's marketplace entry is `"source": "./"` — the plugin IS the
marketplace repo — but pluginMarketplaceUrl only accepted a URL string or
`{url}`, so it returned null and every plugin-cache install answered
`no_remote`: no install of this plugin could ever see a published release.
A relative source now falls back to the manifest's own `repository`. An
`owner/repo` shorthand still returns null — it names a different repo, so
borrowing the marketplace's URL would be a confidently wrong answer.
This commit is contained in:
@@ -64,6 +64,7 @@ function makePluginInstall(name, opts = {}) {
|
|||||||
source = { source: "url", url: "https://forge.example.com/Owner/Repo.git", ref: "main" },
|
source = { source: "url", url: "https://forge.example.com/Owner/Repo.git", ref: "main" },
|
||||||
version = "1.5.1",
|
version = "1.5.1",
|
||||||
knownMarketplaces = true,
|
knownMarketplaces = true,
|
||||||
|
repository,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
const home = path.join(tmpDir, name, ".claude");
|
const home = path.join(tmpDir, name, ".claude");
|
||||||
@@ -74,7 +75,7 @@ function makePluginInstall(name, opts = {}) {
|
|||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(mktDir, ".claude-plugin", "marketplace.json"),
|
path.join(mktDir, ".claude-plugin", "marketplace.json"),
|
||||||
JSON.stringify({ name: "sg", plugins: [{ name: "agenthub", source }] })
|
JSON.stringify({ name: "sg", repository, plugins: [{ name: "agenthub", source }] })
|
||||||
);
|
);
|
||||||
if (knownMarketplaces) {
|
if (knownMarketplaces) {
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -128,10 +129,31 @@ describe("pluginMarketplaceUrl", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to the marketplace's repository for a relative source", () => {
|
||||||
|
// This project's own entry is `"source": "./"` — the plugin IS the
|
||||||
|
// marketplace repo. Without this the dashboard answered `no_remote` and
|
||||||
|
// could never see a release on any plugin-cache install.
|
||||||
|
makePluginInstall("relsrc", {
|
||||||
|
source: "./",
|
||||||
|
repository: "https://forge.example.com/Owner/Repo",
|
||||||
|
});
|
||||||
|
assert.equal(pluginMarketplaceUrl("sg", "agenthub"), "https://forge.example.com/Owner/Repo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for a relative source when the manifest declares no repository", () => {
|
||||||
|
makePluginInstall("relnorepo", { source: "./plugins/agenthub" });
|
||||||
|
assert.equal(pluginMarketplaceUrl("sg", "agenthub"), null);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns null for a github owner/repo shorthand rather than guessing a host", () => {
|
it("returns null for a github owner/repo shorthand rather than guessing a host", () => {
|
||||||
// Inventing github.com here would produce an API URL that 404s and a
|
// Inventing github.com here would produce an API URL that 404s and a
|
||||||
// confidently wrong "up to date".
|
// confidently wrong "up to date".
|
||||||
makePluginInstall("ghshort", { source: { source: "github", repo: "Owner/Repo" } });
|
// Not even when the marketplace declares a repository: a shorthand source
|
||||||
|
// names a DIFFERENT repo, so borrowing the marketplace's is a wrong answer.
|
||||||
|
makePluginInstall("ghshort", {
|
||||||
|
source: { source: "github", repo: "Owner/Repo" },
|
||||||
|
repository: "https://forge.example.com/Owner/Marketplace",
|
||||||
|
});
|
||||||
assert.equal(pluginMarketplaceUrl("sg", "agenthub"), null);
|
assert.equal(pluginMarketplaceUrl("sg", "agenthub"), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,12 @@
|
|||||||
* rather than reporting a false "up to date".
|
* rather than reporting a false "up to date".
|
||||||
*
|
*
|
||||||
* **Two independent paths, chosen by install kind.** A checkout derives the API
|
* **Two independent paths, chosen by install kind.** A checkout derives the API
|
||||||
* URL from its `origin` remote. A plugin-cache install does NOT: when a
|
* URL from its `origin` remote. A plugin-cache install does NOT: Claude Code
|
||||||
* marketplace entry declares `"source": "url"` (this project's own does) Claude
|
* unpacks an archive, so the cache directory has no `.git` and there is no
|
||||||
* Code unpacks an archive, so the cache directory has no `.git` and there is no
|
|
||||||
* remote to read. That path is answered entirely without git — the repository
|
* remote to read. That path is answered entirely without git — the repository
|
||||||
* URL comes from the marketplace manifest on disk (`pluginMarketplaceUrl`) and
|
* URL comes from the marketplace manifest on disk (`pluginMarketplaceUrl`,
|
||||||
|
* which for this project's own relative `"source": "./"` entry resolves to the
|
||||||
|
* manifest's top-level `repository`) and
|
||||||
* the package.json version is the only comparison signal. Getting this wrong is
|
* the package.json version is the only comparison signal. Getting this wrong is
|
||||||
* what shipped in 1.5.1: the `.git` probe ran first, returned "not a git clone",
|
* what shipped in 1.5.1: the `.git` probe ran first, returned "not a git clone",
|
||||||
* and skipped the release layer, so `update_available` stayed false however many
|
* and skipped the release layer, so `update_available` stayed false however many
|
||||||
@@ -346,11 +347,24 @@ function pluginMarketplaceUrl(marketplace, plugin) {
|
|||||||
const entry = (manifest?.plugins || []).find((p) => p && p.name === plugin);
|
const entry = (manifest?.plugins || []).find((p) => p && p.name === plugin);
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
|
|
||||||
// `source` is either a string (a bare URL or owner/repo) or an object with
|
// `source` is either a string (a bare URL, a relative path, or owner/repo)
|
||||||
// its own `source` discriminator plus `url`/`repo`.
|
// or an object with its own `source` discriminator plus `url`/`repo`.
|
||||||
const src = entry.source;
|
const src = entry.source;
|
||||||
if (typeof src === "string") return src.includes("://") ? src : null;
|
if (typeof src === "string" && src.includes("://")) return src;
|
||||||
if (src && typeof src.url === "string" && src.url) return src.url;
|
if (src && typeof src.url === "string" && src.url) return src.url;
|
||||||
|
|
||||||
|
// A RELATIVE source ("./", "./plugins/x") means the plugin ships inside the
|
||||||
|
// marketplace repository itself — this project's own entry is exactly that
|
||||||
|
// ("source": "./") — so the manifest's declared `repository` is where its
|
||||||
|
// releases are published. Without this fallback every plugin-cache install
|
||||||
|
// of this very plugin answered `no_remote` and could never see a release.
|
||||||
|
const relative = typeof src === "string" && /^\.{0,2}\//.test(src);
|
||||||
|
if (relative) {
|
||||||
|
const repo = manifest?.repository;
|
||||||
|
const url = typeof repo === "string" ? repo : repo?.url;
|
||||||
|
if (typeof url === "string" && url.includes("://")) return url;
|
||||||
|
}
|
||||||
|
|
||||||
// A `github`-style `repo: "owner/repo"` shorthand carries no host; this
|
// A `github`-style `repo: "owner/repo"` shorthand carries no host; this
|
||||||
// build is distributed from its own forge, so rather than guessing
|
// build is distributed from its own forge, so rather than guessing
|
||||||
// github.com the caller reports "remote_not_http" and prints the manual
|
// github.com the caller reports "remote_not_http" and prints the manual
|
||||||
|
|||||||
Reference in New Issue
Block a user